diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml new file mode 100644 index 0000000000..c6e9ca4153 --- /dev/null +++ b/.github/actions/restore-python/action.yml @@ -0,0 +1,38 @@ +name: Restore Python +inputs: + python-version: + description: Python version to restore + required: true + type: string + cache-key: + description: Cache key to use + required: true + type: string +outputs: + python-version: + description: Python version restored + value: ${{ steps.python.outputs.python-version }} +runs: + using: "composite" + steps: + - name: Set up Python ${{ inputs.python-version }} + id: python + uses: actions/setup-python@v4.6.0 + with: + python-version: ${{ inputs.python-version }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache/restore@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }} + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + shell: bash + run: | + python -m venv venv + . venv/bin/activate + python --version + pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt + pip install -e . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95e7619a19..7775a996fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,16 @@ jobs: common: name: Create common environment runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 + - name: Generate cache-key + id: cache-key + run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python uses: actions/setup-python@v4.6.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -39,7 +45,7 @@ jobs: with: path: venv # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -66,12 +72,11 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Run black run: | . venv/bin/activate @@ -88,12 +93,11 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Run flake8 run: | . venv/bin/activate @@ -110,12 +114,11 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Run pylint run: | . venv/bin/activate @@ -132,12 +135,11 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Run pyupgrade run: | . venv/bin/activate @@ -154,12 +156,11 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Register matcher run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json" - name: Run script/ci-custom @@ -176,12 +177,11 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Register matcher run: echo "::add-matcher::.github/workflows/matchers/pytest.json" - name: Run pytest @@ -197,12 +197,11 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Install clang-format run: | . venv/bin/activate @@ -237,18 +236,11 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} - - name: Cache platformio - uses: actions/cache@v3.3.1 - with: - path: ~/.platformio - # yamllint disable-line rule:line-length - key: platformio-test${{ matrix.file }}-${{ hashFiles('platformio.ini') }} + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Run esphome compile tests/test${{ matrix.file }}.yaml run: | . venv/bin/activate @@ -300,13 +292,11 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - - name: Restore Python virtual environment - uses: actions/cache/restore@v3.3.1 + - name: Restore Python + uses: ./.github/actions/restore-python with: - path: venv - # yamllint disable-line rule:line-length - key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} - # Use per check platformio cache because checks use different parts + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio uses: actions/cache@v3.3.1 with: diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 896a0369ac..7067300826 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -6,14 +6,12 @@ on: schedule: - cron: '45 6 * * *' -permissions: - contents: write - pull-requests: write jobs: sync: name: Sync Device Classes runs-on: ubuntu-latest + if: github.repository == 'esphome/esphome' steps: - name: Checkout uses: actions/checkout@v3 @@ -38,15 +36,6 @@ jobs: run: | python ./script/sync-device_class.py - - name: Get PR template - id: pr-template-body - run: | - body=$(cat .github/PULL_REQUEST_TEMPLATE.md) - delimiter="$(openssl rand -hex 8)" - echo "body<<$delimiter" >> $GITHUB_OUTPUT - echo "$body" >> $GITHUB_OUTPUT - echo "$delimiter" >> $GITHUB_OUTPUT - - name: Commit changes uses: peter-evans/create-pull-request@v5 with: @@ -56,5 +45,5 @@ jobs: branch: sync/device-classes delete-branch: true title: "Synchronise Device Classes from Home Assistant" - body: ${{ steps.pr-template-body.outputs.body }} + body-path: .github/PULL_REQUEST_TEMPLATE.md token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 617d6f5d9f..9d2eb2908f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.7.0 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/CODEOWNERS b/CODEOWNERS index b4ed234e22..122dc71b48 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,10 +17,11 @@ esphome/components/adc/* @esphome/core esphome/components/adc128s102/* @DeerMaximum esphome/components/addressable_light/* @justfalter esphome/components/airthings_ble/* @jeromelaban -esphome/components/airthings_wave_base/* @jeromelaban @ncareau +esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/alarm_control_panel/* @grahambrown11 +esphome/components/alpha3/* @jan-hofmeier esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix esphome/components/am43/sensor/* @buxtronix @@ -31,6 +32,7 @@ esphome/components/api/* @OttoWinter esphome/components/as7341/* @mrgnr esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl +esphome/components/atm90e26/* @danieltwagner esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter @@ -76,6 +78,7 @@ esphome/components/display_menu_base/* @numo68 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk +esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M esphome/components/ektf2232/* @jesserockz esphome/components/ens210/* @itn3rd77 @@ -102,8 +105,9 @@ esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/graph/* @synco +esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte -esphome/components/haier/* @Yarikx +esphome/components/haier/* @paveldn esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann @@ -200,6 +204,7 @@ esphome/components/output/* @esphome/core esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @hwstar esphome/components/pcf85063/* @brogon +esphome/components/pcf8563/* @KoenBreeman esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie @@ -294,6 +299,7 @@ esphome/components/tof10120/* @wstrzalka esphome/components/toshiba/* @kbx81 esphome/components/touchscreen/* @jesserockz esphome/components/tsl2591/* @wjcarpenter +esphome/components/tt21100/* @kroimon esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/number/* @frankiboy1 @@ -310,6 +316,7 @@ esphome/components/version/* @esphome/core esphome/components/voice_assistant/* @jesserockz esphome/components/wake_on_lan/* @willwill2will54 esphome/components/web_server_base/* @OttoWinter +esphome/components/web_server_idf/* @dentra esphome/components/whirlpool/* @glmnet esphome/components/whynter/* @aeonsablaze esphome/components/wiegand/* @ssieb @@ -319,4 +326,6 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz +esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/* @nielsnl68 @numo68 +esphome/components/zio_ultrasonic/* @kahrendt diff --git a/esphome/__main__.py b/esphome/__main__.py index c7c83ad83b..ecf0092b05 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -32,7 +32,7 @@ from esphome.const import ( SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine -from esphome.helpers import indent +from esphome.helpers import indent, is_ip_address from esphome.util import ( run_external_command, run_external_process, @@ -308,8 +308,10 @@ def upload_program(config, args, host): password = ota_conf.get(CONF_PASSWORD, "") if ( - get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED] - ) and CONF_MQTT in config: + not is_ip_address(CORE.address) + and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]) + and CONF_MQTT in config + ): from esphome import mqtt host = mqtt.get_esphome_device_ip( diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index cceaa594ef..99dad68501 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -24,6 +24,7 @@ ATTENUATION_MODES = { } adc1_channel_t = cg.global_ns.enum("adc1_channel_t") +adc2_channel_t = cg.global_ns.enum("adc2_channel_t") # From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h # pin to adc1 channel mapping @@ -78,6 +79,49 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { }, } +ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { + # TODO: add other variants + VARIANT_ESP32: { + 4: adc2_channel_t.ADC2_CHANNEL_0, + 0: adc2_channel_t.ADC2_CHANNEL_1, + 2: adc2_channel_t.ADC2_CHANNEL_2, + 15: adc2_channel_t.ADC2_CHANNEL_3, + 13: adc2_channel_t.ADC2_CHANNEL_4, + 12: adc2_channel_t.ADC2_CHANNEL_5, + 14: adc2_channel_t.ADC2_CHANNEL_6, + 27: adc2_channel_t.ADC2_CHANNEL_7, + 25: adc2_channel_t.ADC2_CHANNEL_8, + 26: adc2_channel_t.ADC2_CHANNEL_9, + }, + VARIANT_ESP32S2: { + 11: adc2_channel_t.ADC2_CHANNEL_0, + 12: adc2_channel_t.ADC2_CHANNEL_1, + 13: adc2_channel_t.ADC2_CHANNEL_2, + 14: adc2_channel_t.ADC2_CHANNEL_3, + 15: adc2_channel_t.ADC2_CHANNEL_4, + 16: adc2_channel_t.ADC2_CHANNEL_5, + 17: adc2_channel_t.ADC2_CHANNEL_6, + 18: adc2_channel_t.ADC2_CHANNEL_7, + 19: adc2_channel_t.ADC2_CHANNEL_8, + 20: adc2_channel_t.ADC2_CHANNEL_9, + }, + VARIANT_ESP32S3: { + 11: adc2_channel_t.ADC2_CHANNEL_0, + 12: adc2_channel_t.ADC2_CHANNEL_1, + 13: adc2_channel_t.ADC2_CHANNEL_2, + 14: adc2_channel_t.ADC2_CHANNEL_3, + 15: adc2_channel_t.ADC2_CHANNEL_4, + 16: adc2_channel_t.ADC2_CHANNEL_5, + 17: adc2_channel_t.ADC2_CHANNEL_6, + 18: adc2_channel_t.ADC2_CHANNEL_7, + 19: adc2_channel_t.ADC2_CHANNEL_8, + 20: adc2_channel_t.ADC2_CHANNEL_9, + }, + VARIANT_ESP32C3: { + 5: adc2_channel_t.ADC2_CHANNEL_0, + }, +} + def validate_adc_pin(value): if str(value).upper() == "VCC": @@ -89,11 +133,18 @@ def validate_adc_pin(value): if CORE.is_esp32: value = pins.internal_gpio_input_pin_number(value) variant = get_esp32_variant() - if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: + if ( + variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL + and variant not in ESP32_VARIANT_ADC2_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]: + if ( + value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] + and value not in ESP32_VARIANT_ADC2_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: @@ -104,7 +155,7 @@ def validate_adc_pin(value): ) if value != 17: # A0 - raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") + raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") return pins.gpio_pin_schema( {CONF_ANALOG: True, CONF_INPUT: True}, internal=True )(value) @@ -112,7 +163,7 @@ def validate_adc_pin(value): if CORE.is_rp2040: value = pins.internal_gpio_input_pin_number(value) if value not in (26, 27, 28, 29): - raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.") + raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC") return pins.internal_gpio_input_pin_schema(value) raise NotImplementedError diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index 9bfe0f5eed..bb6a7a8c85 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -20,20 +20,20 @@ namespace adc { static const char *const TAG = "adc"; -// 13bit for S2, and 12bit for all other esp32 variants +// 13-bit for S2, 12-bit for all other ESP32 variants #ifdef USE_ESP32 static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast(ADC_WIDTH_MAX - 1); #ifndef SOC_ADC_RTC_MAX_BITWIDTH #if USE_ESP32_VARIANT_ESP32S2 -static const int SOC_ADC_RTC_MAX_BITWIDTH = 13; +static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; #else -static const int SOC_ADC_RTC_MAX_BITWIDTH = 12; +static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; #endif #endif -static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit) -static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit) +static const int32_t ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit) +static const int32_t ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit) #endif #ifdef USE_RP2040 @@ -47,14 +47,21 @@ extern "C" #endif #ifdef USE_ESP32 - adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); - if (!autorange_) { - adc1_config_channel_atten(channel_, attenuation_); + if (channel1_ != ADC1_CHANNEL_MAX) { + adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); + if (!autorange_) { + adc1_config_channel_atten(channel1_, attenuation_); + } + } else if (channel2_ != ADC2_CHANNEL_MAX) { + if (!autorange_) { + adc2_config_channel_atten(channel2_, 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_MAX_SOC_BITS, + for (int32_t i = 0; i < (int32_t) ADC_ATTEN_MAX; i++) { + auto adc_unit = channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; + auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, 1100, // default vref &cal_characteristics_[i]); switch (cal_value) { @@ -136,9 +143,9 @@ void ADCSensor::update() { #ifdef USE_ESP8266 float ADCSensor::sample() { #ifdef USE_ADC_SENSOR_VCC - int raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) + int32_t raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) #else - int raw = analogRead(this->pin_->get_pin()); // NOLINT + int32_t raw = analogRead(this->pin_->get_pin()); // NOLINT #endif if (output_raw_) { return raw; @@ -150,29 +157,53 @@ float ADCSensor::sample() { #ifdef USE_ESP32 float ADCSensor::sample() { if (!autorange_) { - int raw = adc1_get_raw(channel_); + int32_t raw = -1; + if (channel1_ != ADC1_CHANNEL_MAX) { + raw = adc1_get_raw(channel1_); + } else if (channel2_ != ADC2_CHANNEL_MAX) { + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); + } + if (raw == -1) { return NAN; } if (output_raw_) { return raw; } - uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int) attenuation_]); + uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int32_t) attenuation_]); return mv / 1000.0f; } - int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); - raw11 = adc1_get_raw(channel_); - if (raw11 < ADC_MAX) { - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); - raw6 = adc1_get_raw(channel_); - if (raw6 < ADC_MAX) { - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); - raw2 = adc1_get_raw(channel_); - if (raw2 < ADC_MAX) { - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); - raw0 = adc1_get_raw(channel_); + int32_t raw11 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; + + if (channel1_ != ADC1_CHANNEL_MAX) { + adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_11); + raw11 = adc1_get_raw(channel1_); + if (raw11 < ADC_MAX) { + adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_6); + raw6 = adc1_get_raw(channel1_); + if (raw6 < ADC_MAX) { + adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_2_5); + raw2 = adc1_get_raw(channel1_); + if (raw2 < ADC_MAX) { + adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_0); + raw0 = adc1_get_raw(channel1_); + } + } + } + } else if (channel2_ != ADC2_CHANNEL_MAX) { + adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_11); + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw11); + if (raw11 < ADC_MAX) { + adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_6); + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); + if (raw6 < ADC_MAX) { + adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_2_5); + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); + if (raw2 < ADC_MAX) { + adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_0); + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); + } } } } @@ -181,10 +212,10 @@ float ADCSensor::sample() { 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]); + uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_11]); + uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); + uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); + uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) uint32_t c11 = std::min(raw11, ADC_HALF); @@ -212,7 +243,7 @@ float ADCSensor::sample() { adc_select_input(pin - 26); } - int raw = adc_read(); + int32_t raw = adc_read(); if (this->is_temperature_) { adc_set_temp_sensor_enabled(false); } diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 22cddde6f8..a905177790 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -19,16 +19,23 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #ifdef USE_ESP32 /// Set the attenuation for this pin. Only available on the ESP32. void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; } - void set_channel(adc1_channel_t channel) { channel_ = channel; } + void set_channel1(adc1_channel_t channel) { + channel1_ = channel; + channel2_ = ADC2_CHANNEL_MAX; + } + void set_channel2(adc2_channel_t channel) { + channel2_ = channel; + channel1_ = ADC1_CHANNEL_MAX; + } void set_autorange(bool autorange) { autorange_ = autorange; } #endif - /// Update adc values. + /// Update ADC values void update() override; - /// Setup ADc + /// Setup ADC void setup() override; void dump_config() override; - /// `HARDWARE_LATE` setup priority. + /// `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; } @@ -52,9 +59,10 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #ifdef USE_ESP32 adc_atten_t attenuation_{ADC_ATTEN_DB_0}; - adc1_channel_t channel_{}; + adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; + adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; bool autorange_{false}; - esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {}; + esp_adc_cal_characteristics_t cal_characteristics_[(int32_t) ADC_ATTEN_MAX] = {}; #endif }; diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 4695e96570..a0eda1d659 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -1,5 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.core import CORE from esphome.components import sensor, voltage_sampler from esphome.components.esp32 import get_esp32_variant from esphome.const import ( @@ -8,15 +10,15 @@ from esphome.const import ( CONF_NUMBER, CONF_PIN, CONF_RAW, + CONF_WIFI, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) -from esphome.core import CORE - from . import ( ATTENUATION_MODES, ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, + ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, validate_adc_pin, ) @@ -25,7 +27,23 @@ AUTO_LOAD = ["voltage_sampler"] 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.") + raise cv.Invalid("Automatic attenuation cannot be used when raw output is set") + + return config + + +def final_validate_config(config): + if CORE.is_esp32: + variant = get_esp32_variant() + if ( + CONF_WIFI in fv.full_config.get() + and config[CONF_PIN][CONF_NUMBER] + in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] + ): + raise cv.Invalid( + f"{variant} doesn't support ADC on this pin when Wi-Fi is configured" + ) + return config @@ -55,6 +73,8 @@ CONFIG_SCHEMA = cv.All( validate_config, ) +FINAL_VALIDATE_SCHEMA = final_validate_config + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -81,5 +101,15 @@ async def to_code(config): 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)) + if ( + variant in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL + and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] + ): + chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_channel1(chan)) + elif ( + variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL + and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] + ): + chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_channel2(chan)) diff --git a/esphome/components/addressable_light/display.py b/esphome/components/addressable_light/display.py index 0684bf8dfc..5fdd84ac2d 100644 --- a/esphome/components/addressable_light/display.py +++ b/esphome/components/addressable_light/display.py @@ -58,6 +58,6 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/airthings_wave_base/__init__.py b/esphome/components/airthings_wave_base/__init__.py index c935ce108a..d9b97f1c8d 100644 --- a/esphome/components/airthings_wave_base/__init__.py +++ b/esphome/components/airthings_wave_base/__init__.py @@ -3,26 +3,31 @@ import esphome.config_validation as cv from esphome.components import sensor, ble_client from esphome.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, - UNIT_PERCENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, + CONF_BATTERY_VOLTAGE, CONF_HUMIDITY, - CONF_TVOC, CONF_PRESSURE, CONF_TEMPERATURE, + CONF_TVOC, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, UNIT_PARTS_PER_BILLION, - ICON_RADIATOR, + UNIT_PERCENT, + UNIT_VOLT, ) -CODEOWNERS = ["@ncareau", "@jeromelaban"] +CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"] DEPENDENCIES = ["ble_client"] +CONF_BATTERY_UPDATE_INTERVAL = "battery_update_interval" + airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base") AirthingsWaveBase = airthings_wave_base_ns.class_( "AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode @@ -34,9 +39,9 @@ BASE_SCHEMA = ( { cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=0, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, @@ -52,11 +57,21 @@ BASE_SCHEMA = ( ), cv.Optional(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, - icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), + 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, + ), + cv.Optional( + CONF_BATTERY_UPDATE_INTERVAL, + default="24h", + ): cv.update_interval, } ) .extend(cv.polling_component_schema("5min")) @@ -69,15 +84,20 @@ async def wave_base_to_code(var, config): await ble_client.register_ble_node(var, config) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if config_humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(config_humidity) cg.add(var.set_humidity(sens)) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if config_temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(config_temperature) cg.add(var.set_temperature(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) + if config_pressure := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(config_pressure) cg.add(var.set_pressure(sens)) - if CONF_TVOC in config: - sens = await sensor.new_sensor(config[CONF_TVOC]) + if config_tvoc := config.get(CONF_TVOC): + sens = await sensor.new_sensor(config_tvoc) cg.add(var.set_tvoc(sens)) + if config_battery_voltage := config.get(CONF_BATTERY_VOLTAGE): + sens = await sensor.new_sensor(config_battery_voltage) + cg.add(var.set_battery_voltage(sens)) + if config_battery_update_interval := config.get(CONF_BATTERY_UPDATE_INTERVAL): + cg.add(var.set_battery_update_interval(config_battery_update_interval)) diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.cpp b/esphome/components/airthings_wave_base/airthings_wave_base.cpp index 349d8d58eb..16789ff454 100644 --- a/esphome/components/airthings_wave_base/airthings_wave_base.cpp +++ b/esphome/components/airthings_wave_base/airthings_wave_base.cpp @@ -1,5 +1,8 @@ #include "airthings_wave_base.h" +// All information related to reading battery information came from the sensors.airthings_wave +// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave) + #ifdef USE_ESP32 namespace esphome { @@ -18,22 +21,26 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt } case ESP_GATTC_DISCONNECT_EVT: { + this->handle_ = 0; + this->acp_handle_ = 0; + this->cccd_handle_ = 0; ESP_LOGW(TAG, "Disconnected!"); break; } case ESP_GATTC_SEARCH_CMPL_EVT: { - this->handle_ = 0; - auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_); - if (chr == nullptr) { - ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), - this->sensors_data_characteristic_uuid_.to_string().c_str()); - break; + if (this->request_read_values_()) { + if (!this->read_battery_next_update_) { + this->node_state = espbt::ClientState::ESTABLISHED; + } else { + // delay setting node_state to ESTABLISHED until confirmation of the notify registration + this->request_battery_(); + } } - this->handle_ = chr->handle; - this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; - this->request_read_values_(); + // ensure that the client will be disconnected even if no responses arrive + this->set_response_timeout_(); + break; } @@ -50,15 +57,29 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt break; } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->get_conn_id()) + break; + if (param->notify.handle == this->acp_handle_) { + this->read_battery_(param->notify.value, param->notify.value_len); + } + break; + } + default: break; } } -bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } +bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return voc <= 16383; } void AirthingsWaveBase::update() { - if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { if (!this->parent()->enabled) { ESP_LOGW(TAG, "Reconnecting to device"); this->parent()->set_enabled(true); @@ -69,12 +90,119 @@ void AirthingsWaveBase::update() { } } -void AirthingsWaveBase::request_read_values_() { +bool AirthingsWaveBase::request_read_values_() { + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->sensors_data_characteristic_uuid_.to_string().c_str()); + return false; + } + + this->handle_ = chr->handle; + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); + return false; } + + this->response_pending_(); + return true; +} + +bool AirthingsWaveBase::request_battery_() { + uint8_t battery_command = ACCESS_CONTROL_POINT_COMMAND; + uint8_t cccd_value[2] = {1, 0}; + + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s", + this->service_uuid_.to_string().c_str(), + this->access_control_point_characteristic_uuid_.to_string().c_str()); + return false; + } + + auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_, + CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID); + if (descr == nullptr) { + ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->access_control_point_characteristic_uuid_.to_string().c_str()); + return false; + } + + auto reg_status = + esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), chr->handle); + if (reg_status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", reg_status); + return false; + } + + this->acp_handle_ = chr->handle; + this->cccd_handle_ = descr->handle; + + auto descr_status = + esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->cccd_handle_, + 2, cccd_value, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (descr_status) { + ESP_LOGW(TAG, "Error sending CCC descriptor write request, status=%d", descr_status); + return false; + } + + auto chr_status = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->acp_handle_, 1, + &battery_command, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (chr_status) { + ESP_LOGW(TAG, "Error sending read request for battery, status=%d", chr_status); + return false; + } + + this->response_pending_(); + return true; +} + +void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) { + auto *value = (AccessControlPointResponse *) (&raw_value[2]); + + if ((value_len >= (sizeof(AccessControlPointResponse) + 2)) && (raw_value[0] == ACCESS_CONTROL_POINT_COMMAND)) { + ESP_LOGD(TAG, "Battery received: %u mV", (unsigned int) value->battery); + + if (this->battery_voltage_ != nullptr) { + float voltage = value->battery / 1000.0f; + + this->battery_voltage_->publish_state(voltage); + } + + // read the battery again at the configured update interval + if (this->battery_update_interval_ != this->update_interval_) { + this->read_battery_next_update_ = false; + this->set_timeout("battery", this->battery_update_interval_, + [this]() { this->read_battery_next_update_ = true; }); + } + } + + this->response_received_(); +} + +void AirthingsWaveBase::response_pending_() { + this->responses_pending_++; + this->set_response_timeout_(); +} + +void AirthingsWaveBase::response_received_() { + if (--this->responses_pending_ == 0) { + // This instance must not stay connected + // so other clients can connect to it (e.g. the + // mobile app). + this->parent()->set_enabled(false); + } +} + +void AirthingsWaveBase::set_response_timeout_() { + this->set_timeout("response_timeout", 30 * 1000, [this]() { + this->responses_pending_ = 1; + this->response_received_(); + }); } } // namespace airthings_wave_base diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.h b/esphome/components/airthings_wave_base/airthings_wave_base.h index 68c0b3497d..1dc2e1f71f 100644 --- a/esphome/components/airthings_wave_base/airthings_wave_base.h +++ b/esphome/components/airthings_wave_base/airthings_wave_base.h @@ -1,5 +1,8 @@ #pragma once +// All information related to reading battery levels came from the sensors.airthings_wave +// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave) + #ifdef USE_ESP32 #include @@ -14,6 +17,11 @@ namespace esphome { namespace airthings_wave_base { +namespace espbt = esphome::esp32_ble_tracker; + +static const uint8_t ACCESS_CONTROL_POINT_COMMAND = 0x6d; +static const auto CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID = espbt::ESPBTUUID::from_uint16(0x2902); + class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode { public: AirthingsWaveBase() = default; @@ -27,21 +35,53 @@ class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientN void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + void set_battery_voltage(sensor::Sensor *voltage) { + battery_voltage_ = voltage; + this->read_battery_next_update_ = true; + } + void set_battery_update_interval(uint32_t interval) { battery_update_interval_ = interval; } protected: bool is_valid_voc_value_(uint16_t voc); - virtual void read_sensors(uint8_t *value, uint16_t value_len) = 0; - void request_read_values_(); + bool request_read_values_(); + virtual void read_sensors(uint8_t *raw_value, uint16_t value_len) = 0; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; sensor::Sensor *tvoc_sensor_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; uint16_t handle_; - esp32_ble_tracker::ESPBTUUID service_uuid_; - esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID sensors_data_characteristic_uuid_; + + uint16_t acp_handle_{0}; + uint16_t cccd_handle_{0}; + espbt::ESPBTUUID access_control_point_characteristic_uuid_; + + uint8_t responses_pending_{0}; + void response_pending_(); + void response_received_(); + void set_response_timeout_(); + + // default to *not* reading battery voltage from the device; the + // set_* function for the battery sensor will set this to 'true' + bool read_battery_next_update_{false}; + bool request_battery_(); + void read_battery_(uint8_t *raw_value, uint16_t value_len); + uint32_t battery_update_interval_{}; + + struct AccessControlPointResponse { + uint32_t unused1; + uint8_t unused2; + uint8_t illuminance; + uint8_t unused3[10]; + uint16_t unused4[4]; + uint16_t battery; + uint16_t unused5; + }; }; } // namespace airthings_wave_base diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp index 331a13434f..873826d06c 100644 --- a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp @@ -26,12 +26,9 @@ void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) { if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { this->tvoc_sensor_->publish_state(value->voc); } - - // This instance must not stay connected - // so other clients can connect to it (e.g. the - // mobile app). - this->parent()->set_enabled(false); } + + this->response_received_(); } void AirthingsWaveMini::dump_config() { @@ -42,11 +39,14 @@ void AirthingsWaveMini::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); } AirthingsWaveMini::AirthingsWaveMini() { - this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); - this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); + this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->access_control_point_characteristic_uuid_ = + espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); } } // namespace airthings_wave_mini diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.h b/esphome/components/airthings_wave_mini/airthings_wave_mini.h index ec4fd23e60..825ddbdc69 100644 --- a/esphome/components/airthings_wave_mini/airthings_wave_mini.h +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.h @@ -7,8 +7,11 @@ namespace esphome { namespace airthings_wave_mini { +namespace espbt = esphome::esp32_ble_tracker; + static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba"; +static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e3ef4-ade7-11e4-89d3-123b93f75cba"; class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase { public: @@ -17,7 +20,7 @@ class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase { void dump_config() override; protected: - void read_sensors(uint8_t *value, uint16_t value_len) override; + void read_sensors(uint8_t *raw_value, uint16_t value_len) override; struct WaveMiniReadings { uint16_t unused01; diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp index acd3a4316d..a32128e992 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -43,20 +43,17 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { this->tvoc_sensor_->publish_state(value->voc); } - - // This instance must not stay connected - // so other clients can connect to it (e.g. the - // mobile app). - this->parent()->set_enabled(false); } else { ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); } } + + this->response_received_(); } -bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } +bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return radon <= 16383; } -bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; } +bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return co2 <= 16383; } void AirthingsWavePlus::dump_config() { // these really don't belong here, but there doesn't seem to be a @@ -66,6 +63,7 @@ void AirthingsWavePlus::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); LOG_SENSOR(" ", "Radon", this->radon_sensor_); LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); @@ -73,8 +71,10 @@ void AirthingsWavePlus::dump_config() { } AirthingsWavePlus::AirthingsWavePlus() { - this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); - this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); + this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->access_control_point_characteristic_uuid_ = + espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); } } // namespace airthings_wave_plus diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h index 4acfb9279a..23c8cbb166 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.h +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -7,8 +7,11 @@ namespace esphome { namespace airthings_wave_plus { +namespace espbt = esphome::esp32_ble_tracker; + static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; +static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba"; class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { public: @@ -24,7 +27,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { bool is_valid_radon_value_(uint16_t radon); bool is_valid_co2_value_(uint16_t co2); - void read_sensors(uint8_t *value, uint16_t value_len) override; + void read_sensors(uint8_t *raw_value, uint16_t value_len) override; sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr}; diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py index a5903b1d42..643a2bfb68 100644 --- a/esphome/components/airthings_wave_plus/sensor.py +++ b/esphome/components/airthings_wave_plus/sensor.py @@ -53,12 +53,12 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await airthings_wave_base.wave_base_to_code(var, config) - if CONF_RADON in config: - sens = await sensor.new_sensor(config[CONF_RADON]) + if config_radon := config.get(CONF_RADON): + sens = await sensor.new_sensor(config_radon) cg.add(var.set_radon(sens)) - if CONF_RADON_LONG_TERM in config: - sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) + if config_radon_long_term := config.get(CONF_RADON_LONG_TERM): + sens = await sensor.new_sensor(config_radon_long_term) cg.add(var.set_radon_long_term(sens)) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if config_co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(config_co2) cg.add(var.set_co2(sens)) diff --git a/esphome/components/alpha3/__init__.py b/esphome/components/alpha3/__init__.py new file mode 100644 index 0000000000..7cd320c80f --- /dev/null +++ b/esphome/components/alpha3/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jan-hofmeier"] diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp new file mode 100644 index 0000000000..17899c31cb --- /dev/null +++ b/esphome/components/alpha3/alpha3.cpp @@ -0,0 +1,189 @@ +#include "alpha3.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include //gives ntohl + +#ifdef USE_ESP32 + +namespace esphome { +namespace alpha3 { + +static const char *const TAG = "alpha3"; + +void Alpha3::dump_config() { + ESP_LOGCONFIG(TAG, "ALPHA3"); + LOG_SENSOR(" ", "Flow", this->flow_sensor_); + LOG_SENSOR(" ", "Head", this->head_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Speed", this->speed_sensor_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); +} + +void Alpha3::setup() {} + +void Alpha3::extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset, + int16_t value_offset, sensor::Sensor *sensor, float factor) { + if (sensor == nullptr) + return; + // we need to handle cases where a value is split over two packets + const int16_t value_length = 4; // 32bit float + // offset inside current response packet + auto rel_offset = value_offset - response_offset; + if (rel_offset <= -value_length) + return; // aready passed the value completly + if (rel_offset >= length) + return; // value not in this packet + + auto start_offset = std::max(0, rel_offset); + auto end_offset = std::min((int16_t) (rel_offset + value_length), length); + auto copy_length = end_offset - start_offset; + auto buffer_offset = std::max(-rel_offset, 0); + std::memcpy(this->buffer_ + buffer_offset, response + start_offset, copy_length); + + if (rel_offset + value_length <= length) { + // we have the whole value + void *buffer = this->buffer_; // to prevent warnings when casting the pointer + *((int32_t *) buffer) = ntohl(*((int32_t *) buffer)); // values are big endian + float fvalue = *((float *) buffer); + sensor->publish_state(fvalue * factor); + } +} + +bool Alpha3::is_current_response_type_(const uint8_t *response_type) { + return !std::memcmp(this->response_type_, response_type, GENI_RESPONSE_TYPE_LENGTH); +} + +void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { + if (this->response_offset_ >= this->response_length_) { + ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str()); + if (length < GENI_RESPONSE_HEADER_LENGTH) { + ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str()); + return; + } + if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) { + ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(), + response[0], response[1], response[2], response[3], response[4]); + return; + } + this->response_length_ = response[1] - GENI_RESPONSE_HEADER_LENGTH + 2; // maybe 2 byte checksum + this->response_offset_ = -GENI_RESPONSE_HEADER_LENGTH; + std::memcpy(this->response_type_, response + 5, GENI_RESPONSE_TYPE_LENGTH); + } + + auto extract_publish_sensor_value = [response, length, this](int16_t value_offset, sensor::Sensor *sensor, + float factor) { + this->extract_publish_sensor_value_(response, length, this->response_offset_, value_offset, sensor, factor); + }; + + if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) { + ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str()); + extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F); + extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F); + } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) { + ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str()); + extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F); + extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F); + extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F); + extract_publish_sensor_value(GENI_RESPONSE_VOLTAGE_AC_OFFSET, this->voltage_sensor_, 1.0F); + } else { + ESP_LOGW(TAG, "unkown GENI response Type %d %d %d %d %d %d %d %d", this->response_type_[0], this->response_type_[1], + this->response_type_[2], this->response_type_[3], this->response_type_[4], this->response_type_[5], + this->response_type_[6], this->response_type_[7]); + } + this->response_offset_ += length; +} + +void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + this->response_offset_ = 0; + this->response_length_ = 0; + ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str()); + break; + } + case ESP_GATTC_CONNECT_EVT: { + if (std::memcmp(param->connect.remote_bda, this->parent_->get_remote_bda(), 6) != 0) + return; + auto ret = esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT); + if (ret) { + ESP_LOGW(TAG, "esp_ble_set_encryption failed, status=%x", ret); + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + this->node_state = espbt::ClientState::IDLE; + if (this->flow_sensor_ != nullptr) + this->flow_sensor_->publish_state(NAN); + if (this->head_sensor_ != nullptr) + this->head_sensor_->publish_state(NAN); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(NAN); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(NAN); + if (this->speed_sensor_ != nullptr) + this->speed_sensor_->publish_state(NAN); + if (this->speed_sensor_ != nullptr) + this->voltage_sensor_->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID); + if (chr == nullptr) { + ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str()); + break; + } + auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), + chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + this->geni_handle_ = chr->handle; + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + this->update(); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle == this->geni_handle_) { + this->handle_geni_response_(param->notify.value, param->notify.value_len); + } + break; + } + default: + break; + } +} + +void Alpha3::send_request_(uint8_t *request, size_t len) { + auto status = + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len, + request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); +} + +void Alpha3::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + return; + } + + if (this->flow_sensor_ != nullptr || this->head_sensor_ != nullptr) { + uint8_t geni_request_flow_head[] = {39, 7, 231, 248, 10, 3, 93, 1, 33, 82, 31}; + this->send_request_(geni_request_flow_head, sizeof(geni_request_flow_head)); + delay(25); // need to wait between requests + } + if (this->power_sensor_ != nullptr || this->current_sensor_ != nullptr || this->speed_sensor_ != nullptr || + this->voltage_sensor_ != nullptr) { + uint8_t geni_request_power[] = {39, 7, 231, 248, 10, 3, 87, 0, 69, 138, 205}; + this->send_request_(geni_request_power, sizeof(geni_request_power)); + delay(25); // need to wait between requests + } +} +} // namespace alpha3 +} // namespace esphome + +#endif diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h new file mode 100644 index 0000000000..325c70a538 --- /dev/null +++ b/esphome/components/alpha3/alpha3.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace alpha3 { + +namespace espbt = esphome::esp32_ble_tracker; + +static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d); +static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID = + espbt::ESPBTUUID::from_raw({static_cast(0xa9), 0x7b, static_cast(0xb8), static_cast(0x85), 0x0, + 0x1a, 0x28, static_cast(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast(0xd1), + static_cast(0xff), static_cast(0x9c), static_cast(0x85)}); +static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13; +static const size_t GENI_RESPONSE_TYPE_LENGTH = 8; + +static const uint8_t GENI_RESPONSE_TYPE_FLOW_HEAD[GENI_RESPONSE_TYPE_LENGTH] = {31, 0, 1, 48, 1, 0, 0, 24}; +static const int16_t GENI_RESPONSE_FLOW_OFFSET = 0; +static const int16_t GENI_RESPONSE_HEAD_OFFSET = 4; + +static const uint8_t GENI_RESPONSE_TYPE_POWER[GENI_RESPONSE_TYPE_LENGTH] = {44, 0, 1, 0, 1, 0, 0, 37}; +static const int16_t GENI_RESPONSE_VOLTAGE_AC_OFFSET = 0; +static const int16_t GENI_RESPONSE_VOLTAGE_DC_OFFSET = 4; +static const int16_t GENI_RESPONSE_CURRENT_OFFSET = 8; +static const int16_t GENI_RESPONSE_POWER_OFFSET = 12; +static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16; // not sure +static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20; + +class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void setup() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } + void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; } + void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } + void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; } + void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; } + void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sensor_ = sensor; } + + protected: + sensor::Sensor *flow_sensor_{nullptr}; + sensor::Sensor *head_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *speed_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + uint16_t geni_handle_; + int16_t response_length_; + int16_t response_offset_; + uint8_t response_type_[GENI_RESPONSE_TYPE_LENGTH]; + uint8_t buffer_[4]; + void extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset, + int16_t value_offset, sensor::Sensor *sensor, float factor); + void handle_geni_response_(const uint8_t *response, uint16_t length); + void send_request_(uint8_t *request, size_t len); + bool is_current_response_type_(const uint8_t *response_type); +}; +} // namespace alpha3 +} // namespace esphome + +#endif diff --git a/esphome/components/alpha3/sensor.py b/esphome/components/alpha3/sensor.py new file mode 100644 index 0000000000..ba4ca16a5a --- /dev/null +++ b/esphome/components/alpha3/sensor.py @@ -0,0 +1,85 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client +from esphome.const import ( + CONF_ID, + CONF_CURRENT, + CONF_FLOW, + CONF_HEAD, + CONF_POWER, + CONF_SPEED, + CONF_VOLTAGE, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_WATT, + UNIT_METER, + UNIT_CUBIC_METER_PER_HOUR, + UNIT_REVOLUTIONS_PER_MINUTE, +) + +alpha3_ns = cg.esphome_ns.namespace("alpha3") +Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Alpha3), + cv.Optional(CONF_FLOW): sensor.sensor_schema( + unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR, + accuracy_decimals=2, + ), + cv.Optional(CONF_HEAD): sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + accuracy_decimals=2, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + accuracy_decimals=2, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + ), + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("15s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + + if CONF_FLOW in config: + sens = await sensor.new_sensor(config[CONF_FLOW]) + cg.add(var.set_flow_sensor(sens)) + + if CONF_HEAD in config: + sens = await sensor.new_sensor(config[CONF_HEAD]) + cg.add(var.set_head_sensor(sens)) + + if CONF_POWER in config: + sens = await sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) + + if CONF_CURRENT in config: + sens = await sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + + if CONF_SPEED in config: + sens = await sensor.new_sensor(config[CONF_SPEED]) + cg.add(var.set_speed_sensor(sens)) + + if CONF_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_VOLTAGE]) + cg.add(var.set_voltage_sensor(sens)) diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index f51d115d9e..82e724fa00 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -1,7 +1,7 @@ import logging -from esphome import core -from esphome.components import display, font +from esphome import automation, core +from esphome.components import font import esphome.components.image as espImage from esphome.components.image import CONF_USE_TRANSPARENCY import esphome.config_validation as cv @@ -18,14 +18,30 @@ from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) +AUTO_LOAD = ["image"] +CODEOWNERS = ["@syndlex"] DEPENDENCIES = ["display"] MULTI_CONF = True CONF_LOOP = "loop" CONF_START_FRAME = "start_frame" CONF_END_FRAME = "end_frame" +CONF_FRAME = "frame" -Animation_ = display.display_ns.class_("Animation", espImage.Image_) +animation_ns = cg.esphome_ns.namespace("animation") + +Animation_ = animation_ns.class_("Animation", espImage.Image_) + +# Actions +NextFrameAction = animation_ns.class_( + "AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_) +) +PrevFrameAction = animation_ns.class_( + "AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_) +) +SetFrameAction = animation_ns.class_( + "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) +) def validate_cross_dependencies(config): @@ -74,7 +90,35 @@ ANIMATION_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) -CODEOWNERS = ["@syndlex"] +NEXT_FRAME_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(Animation_), + } +) +PREV_FRAME_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(Animation_), + } +) +SET_FRAME_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(Animation_), + cv.Required(CONF_FRAME): cv.uint16_t, + } +) + + +@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA) +@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA) +@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA) +async def animation_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + if CONF_FRAME in config: + template_ = await cg.templatable(config[CONF_FRAME], args, cg.uint16) + cg.add(var.set_frame(template_)) + return var async def to_code(config): diff --git a/esphome/components/display/animation.cpp b/esphome/components/animation/animation.cpp similarity index 94% rename from esphome/components/display/animation.cpp rename to esphome/components/animation/animation.cpp index d68084b68d..7e0efa97e0 100644 --- a/esphome/components/display/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -3,9 +3,10 @@ #include "esphome/core/hal.h" namespace esphome { -namespace display { +namespace animation { -Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) +Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, + image::ImageType type) : Image(data_start, width, height, type), animation_data_start_(data_start), current_frame_(0), @@ -65,5 +66,5 @@ void Animation::update_data_start_() { this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; } -} // namespace display +} // namespace animation } // namespace esphome diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h new file mode 100644 index 0000000000..272c5153d1 --- /dev/null +++ b/esphome/components/animation/animation.h @@ -0,0 +1,67 @@ +#pragma once +#include "esphome/components/image/image.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace animation { + +class Animation : public image::Image { + public: + Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type); + + uint32_t get_animation_frame_count() const; + int get_current_frame() const; + void next_frame(); + void prev_frame(); + + /** Selects a specific frame within the animation. + * + * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame. + */ + void set_frame(int frame); + + void set_loop(uint32_t start_frame, uint32_t end_frame, int count); + + protected: + void update_data_start_(); + + const uint8_t *animation_data_start_; + int current_frame_; + uint32_t animation_frame_count_; + uint32_t loop_start_frame_; + uint32_t loop_end_frame_; + int loop_count_; + int loop_current_iteration_; +}; + +template class AnimationNextFrameAction : public Action { + public: + AnimationNextFrameAction(Animation *parent) : parent_(parent) {} + void play(Ts... x) override { this->parent_->next_frame(); } + + protected: + Animation *parent_; +}; + +template class AnimationPrevFrameAction : public Action { + public: + AnimationPrevFrameAction(Animation *parent) : parent_(parent) {} + void play(Ts... x) override { this->parent_->prev_frame(); } + + protected: + Animation *parent_; +}; + +template class AnimationSetFrameAction : public Action { + public: + AnimationSetFrameAction(Animation *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(uint16_t, frame) + void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); } + + protected: + Animation *parent_; +}; + +} // namespace animation +} // namespace esphome diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 0d68d9fe55..86685aa5e6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1420,6 +1420,7 @@ message VoiceAssistantRequest { bool start = 1; string conversation_id = 2; + bool use_vad = 3; } message VoiceAssistantResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 858ff0e525..a46efd80e5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -907,12 +907,13 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_ #endif #ifdef USE_VOICE_ASSISTANT -bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id) { +bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id, bool use_vad) { if (!this->voice_assistant_subscription_) return false; VoiceAssistantRequest msg; msg.start = start; msg.conversation_id = conversation_id; + msg.use_vad = use_vad; return this->send_voice_assistant_request(msg); } void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index c146adff02..acc4578661 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -124,7 +124,7 @@ class APIConnection : public APIServerConnection { void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override { this->voice_assistant_subscription_ = msg.subscribe; } - bool request_voice_assistant(bool start, const std::string &conversation_id); + bool request_voice_assistant(bool start, const std::string &conversation_id, bool use_vad); void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8c7f6d0c4a..3a2d980e57 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -6348,6 +6348,10 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->start = value.as_bool(); return true; } + case 3: { + this->use_vad = value.as_bool(); + return true; + } default: return false; } @@ -6365,6 +6369,7 @@ bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimite void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); + buffer.encode_bool(3, this->use_vad); } #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantRequest::dump_to(std::string &out) const { @@ -6377,6 +6382,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append(" conversation_id: "); out.append("'").append(this->conversation_id).append("'"); out.append("\n"); + + out.append(" use_vad: "); + out.append(YESNO(this->use_vad)); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 769f7aaff5..627165953d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1655,6 +1655,7 @@ class VoiceAssistantRequest : public ProtoMessage { public: bool start{false}; std::string conversation_id{}; + bool use_vad{false}; 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 87b5f9e63f..f70d45ecd0 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -323,16 +323,16 @@ void APIServer::on_shutdown() { } #ifdef USE_VOICE_ASSISTANT -bool APIServer::start_voice_assistant(const std::string &conversation_id) { +bool APIServer::start_voice_assistant(const std::string &conversation_id, bool use_vad) { for (auto &c : this->clients_) { - if (c->request_voice_assistant(true, conversation_id)) + if (c->request_voice_assistant(true, conversation_id, use_vad)) return true; } return false; } void APIServer::stop_voice_assistant() { for (auto &c : this->clients_) { - if (c->request_voice_assistant(false, "")) + if (c->request_voice_assistant(false, "", false)) return; } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index be124f42ff..9b40a5ef02 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -81,7 +81,7 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_VOICE_ASSISTANT - bool start_voice_assistant(const std::string &conversation_id); + bool start_voice_assistant(const std::string &conversation_id, bool use_vad); void stop_voice_assistant(); #endif diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index c777c3be9d..819055ccf4 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -47,7 +47,7 @@ async def async_run_logs(config, address): except APIConnectionError: cli.disconnect() - async def on_disconnect(): + async def on_disconnect(expected_disconnect: bool) -> None: _LOGGER.warning("Disconnected from API") zc = zeroconf.Zeroconf() diff --git a/esphome/components/atm90e26/__init__.py b/esphome/components/atm90e26/__init__.py new file mode 100644 index 0000000000..ac441a9c2d --- /dev/null +++ b/esphome/components/atm90e26/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@danieltwagner"] diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp new file mode 100644 index 0000000000..42a52c4ccf --- /dev/null +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -0,0 +1,235 @@ +#include "atm90e26.h" +#include "atm90e26_reg.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace atm90e26 { + +static const char *const TAG = "atm90e26"; + +void ATM90E26Component::update() { + if (this->read16_(ATM90E26_REGISTER_FUNCEN) != 0x0030) { + this->status_set_warning(); + return; + } + + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(this->get_line_voltage_()); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(this->get_line_current_()); + } + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(this->get_active_power_()); + } + if (this->reactive_power_sensor_ != nullptr) { + this->reactive_power_sensor_->publish_state(this->get_reactive_power_()); + } + if (this->power_factor_sensor_ != nullptr) { + this->power_factor_sensor_->publish_state(this->get_power_factor_()); + } + if (this->forward_active_energy_sensor_ != nullptr) { + this->forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_()); + } + if (this->reverse_active_energy_sensor_ != nullptr) { + this->reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_()); + } + if (this->freq_sensor_ != nullptr) { + this->freq_sensor_->publish_state(this->get_frequency_()); + } + this->status_clear_warning(); +} + +void ATM90E26Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ATM90E26 Component..."); + this->spi_setup(); + + uint16_t mmode = 0x422; // default values for everything but L/N line current gains + mmode |= (gain_pga_ & 0x7) << 13; + mmode |= (n_line_gain_ & 0x3) << 11; + + this->write16_(ATM90E26_REGISTER_SOFTRESET, 0x789A); // Perform soft reset + this->write16_(ATM90E26_REGISTER_FUNCEN, + 0x0030); // Voltage sag irq=1, report on warnout pin=1, energy dir change irq=0 + uint16_t read = this->read16_(ATM90E26_REGISTER_LASTDATA); + if (read != 0x0030) { + ESP_LOGW(TAG, "Could not initialize ATM90E26 IC, check SPI settings: %d", read); + this->mark_failed(); + return; + } + // TODO: 100 * * sqrt(2) * / (4 * gain_voltage/32768) + this->write16_(ATM90E26_REGISTER_SAGTH, 0x17DD); // Voltage sag threshhold 0x1F2F + + // Set metering calibration values + this->write16_(ATM90E26_REGISTER_CALSTART, 0x5678); // CAL Metering calibration startup command + + // Configure + this->write16_(ATM90E26_REGISTER_MMODE, mmode); // Metering Mode Configuration (see above) + + this->write16_(ATM90E26_REGISTER_PLCONSTH, (pl_const_ >> 16)); // PL Constant MSB + this->write16_(ATM90E26_REGISTER_PLCONSTL, pl_const_ & 0xFFFF); // PL Constant LSB + + // Calibrate this to be 1 pulse per Wh + this->write16_(ATM90E26_REGISTER_LGAIN, gain_metering_); // L Line Calibration Gain (active power metering) + this->write16_(ATM90E26_REGISTER_LPHI, 0x0000); // L Line Calibration Angle + this->write16_(ATM90E26_REGISTER_NGAIN, 0x0000); // N Line Calibration Gain + this->write16_(ATM90E26_REGISTER_NPHI, 0x0000); // N Line Calibration Angle + this->write16_(ATM90E26_REGISTER_PSTARTTH, 0x08BD); // Active Startup Power Threshold (default) = 2237 + this->write16_(ATM90E26_REGISTER_PNOLTH, 0x0000); // Active No-Load Power Threshold + this->write16_(ATM90E26_REGISTER_QSTARTTH, 0x0AEC); // Reactive Startup Power Threshold (default) = 2796 + this->write16_(ATM90E26_REGISTER_QNOLTH, 0x0000); // Reactive No-Load Power Threshold + + // Compute Checksum for the registers we set above + // low byte = sum of all bytes + uint16_t cs = + ((mmode >> 8) + (mmode & 0xFF) + (pl_const_ >> 24) + ((pl_const_ >> 16) & 0xFF) + ((pl_const_ >> 8) & 0xFF) + + (pl_const_ & 0xFF) + (gain_metering_ >> 8) + (gain_metering_ & 0xFF) + 0x08 + 0xBD + 0x0A + 0xEC) & + 0xFF; + // high byte = XOR of all bytes + cs |= ((mmode >> 8) ^ (mmode & 0xFF) ^ (pl_const_ >> 24) ^ ((pl_const_ >> 16) & 0xFF) ^ ((pl_const_ >> 8) & 0xFF) ^ + (pl_const_ & 0xFF) ^ (gain_metering_ >> 8) ^ (gain_metering_ & 0xFF) ^ 0x08 ^ 0xBD ^ 0x0A ^ 0xEC) + << 8; + + this->write16_(ATM90E26_REGISTER_CS1, cs); + ESP_LOGVV(TAG, "Set CS1 to: 0x%04X", cs); + + // Set measurement calibration values + this->write16_(ATM90E26_REGISTER_ADJSTART, 0x5678); // Measurement calibration startup command, registers 31-3A + this->write16_(ATM90E26_REGISTER_UGAIN, gain_voltage_); // Voltage RMS gain + this->write16_(ATM90E26_REGISTER_IGAINL, gain_ct_); // L line current RMS gain + this->write16_(ATM90E26_REGISTER_IGAINN, 0x7530); // N Line Current RMS Gain + this->write16_(ATM90E26_REGISTER_UOFFSET, 0x0000); // Voltage Offset + this->write16_(ATM90E26_REGISTER_IOFFSETL, 0x0000); // L Line Current Offset + this->write16_(ATM90E26_REGISTER_IOFFSETN, 0x0000); // N Line Current Offse + this->write16_(ATM90E26_REGISTER_POFFSETL, 0x0000); // L Line Active Power Offset + this->write16_(ATM90E26_REGISTER_QOFFSETL, 0x0000); // L Line Reactive Power Offset + this->write16_(ATM90E26_REGISTER_POFFSETN, 0x0000); // N Line Active Power Offset + this->write16_(ATM90E26_REGISTER_QOFFSETN, 0x0000); // N Line Reactive Power Offset + + // Compute Checksum for the registers we set above + cs = ((gain_voltage_ >> 8) + (gain_voltage_ & 0xFF) + (gain_ct_ >> 8) + (gain_ct_ & 0xFF) + 0x75 + 0x30) & 0xFF; + cs |= ((gain_voltage_ >> 8) ^ (gain_voltage_ & 0xFF) ^ (gain_ct_ >> 8) ^ (gain_ct_ & 0xFF) ^ 0x75 ^ 0x30) << 8; + this->write16_(ATM90E26_REGISTER_CS2, cs); + ESP_LOGVV(TAG, "Set CS2 to: 0x%04X", cs); + + this->write16_(ATM90E26_REGISTER_CALSTART, + 0x8765); // Checks correctness of 21-2B registers and starts normal metering if ok + this->write16_(ATM90E26_REGISTER_ADJSTART, + 0x8765); // Checks correctness of 31-3A registers and starts normal measurement if ok + + uint16_t sys_status = this->read16_(ATM90E26_REGISTER_SYSSTATUS); + if (sys_status & 0xC000) { // Checksum 1 Error + + ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS1 was incorrect, expected: 0x%04X", + this->read16_(ATM90E26_REGISTER_CS1)); + this->mark_failed(); + } + if (sys_status & 0x3000) { // Checksum 2 Error + ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS2 was incorrect, expected: 0x%04X", + this->read16_(ATM90E26_REGISTER_CS2)); + this->mark_failed(); + } +} + +void ATM90E26Component::dump_config() { + ESP_LOGCONFIG("", "ATM90E26:"); + LOG_PIN(" CS Pin: ", this->cs_); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with ATM90E26 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Voltage A", this->voltage_sensor_); + LOG_SENSOR(" ", "Current A", this->current_sensor_); + LOG_SENSOR(" ", "Power A", this->power_sensor_); + LOG_SENSOR(" ", "Reactive Power A", this->reactive_power_sensor_); + LOG_SENSOR(" ", "PF A", this->power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy A", this->forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_); + LOG_SENSOR(" ", "Frequency", this->freq_sensor_); +} +float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; } + +uint16_t ATM90E26Component::read16_(uint8_t a_register) { + uint8_t data[2]; + uint16_t output; + + this->enable(); + delayMicroseconds(4); + this->write_byte(a_register | 0x80); + delayMicroseconds(4); + this->read_array(data, 2); + this->disable(); + + output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); + ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output); + return output; +} + +void ATM90E26Component::write16_(uint8_t a_register, uint16_t val) { + ESP_LOGVV(TAG, "write16_ 0x%04X val 0x%04X", a_register, val); + this->enable(); + delayMicroseconds(4); + this->write_byte(a_register & 0x7F); + delayMicroseconds(4); + this->write_byte((val >> 8) & 0xFF); + this->write_byte(val & 0xFF); + this->disable(); +} + +float ATM90E26Component::get_line_current_() { + uint16_t current = this->read16_(ATM90E26_REGISTER_IRMS); + return current / 1000.0f; +} + +float ATM90E26Component::get_line_voltage_() { + uint16_t voltage = this->read16_(ATM90E26_REGISTER_URMS); + return voltage / 100.0f; +} + +float ATM90E26Component::get_active_power_() { + int16_t val = this->read16_(ATM90E26_REGISTER_PMEAN); // two's complement + return (float) val; +} + +float ATM90E26Component::get_reactive_power_() { + int16_t val = this->read16_(ATM90E26_REGISTER_QMEAN); // two's complement + return (float) val; +} + +float ATM90E26Component::get_power_factor_() { + uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed + if (val & 0x8000) { + return -(val & 0x7FF) / 1000.0f; + } else { + return val / 1000.0f; + } +} + +float ATM90E26Component::get_forward_active_energy_() { + uint16_t val = this->read16_(ATM90E26_REGISTER_APENERGY); + if ((UINT32_MAX - this->cumulative_forward_active_energy_) > val) { + this->cumulative_forward_active_energy_ += val; + } else { + this->cumulative_forward_active_energy_ = val; + } + // The register holds thenths of pulses, we want to output Wh + return (this->cumulative_forward_active_energy_ * 100.0f / meter_constant_); +} + +float ATM90E26Component::get_reverse_active_energy_() { + uint16_t val = this->read16_(ATM90E26_REGISTER_ANENERGY); + if (UINT32_MAX - this->cumulative_reverse_active_energy_ > val) { + this->cumulative_reverse_active_energy_ += val; + } else { + this->cumulative_reverse_active_energy_ = val; + } + return (this->cumulative_reverse_active_energy_ * 100.0f / meter_constant_); +} + +float ATM90E26Component::get_frequency_() { + uint16_t freq = this->read16_(ATM90E26_REGISTER_FREQ); + return freq / 100.0f; +} + +} // namespace atm90e26 +} // namespace esphome diff --git a/esphome/components/atm90e26/atm90e26.h b/esphome/components/atm90e26/atm90e26.h new file mode 100644 index 0000000000..3c098d7e91 --- /dev/null +++ b/esphome/components/atm90e26/atm90e26.h @@ -0,0 +1,72 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace atm90e26 { + +class ATM90E26Component : public PollingComponent, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; } + void set_current_sensor(sensor::Sensor *obj) { this->current_sensor_ = obj; } + void set_power_sensor(sensor::Sensor *obj) { this->power_sensor_ = obj; } + void set_reactive_power_sensor(sensor::Sensor *obj) { this->reactive_power_sensor_ = obj; } + void set_forward_active_energy_sensor(sensor::Sensor *obj) { this->forward_active_energy_sensor_ = obj; } + void set_reverse_active_energy_sensor(sensor::Sensor *obj) { this->reverse_active_energy_sensor_ = obj; } + void set_power_factor_sensor(sensor::Sensor *obj) { this->power_factor_sensor_ = obj; } + void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } + void set_line_freq(int freq) { line_freq_ = freq; } + void set_meter_constant(float val) { meter_constant_ = val; } + void set_pl_const(uint32_t pl_const) { pl_const_ = pl_const; } + void set_gain_metering(uint16_t gain) { this->gain_metering_ = gain; } + void set_gain_voltage(uint16_t gain) { this->gain_voltage_ = gain; } + void set_gain_ct(uint16_t gain) { this->gain_ct_ = gain; } + void set_gain_pga(uint16_t gain) { gain_pga_ = gain; } + void set_n_line_gain(uint16_t gain) { n_line_gain_ = gain; } + + protected: + uint16_t read16_(uint8_t a_register); + int read32_(uint8_t addr_h, uint8_t addr_l); + void write16_(uint8_t a_register, uint16_t val); + + float get_line_voltage_(); + float get_line_current_(); + float get_active_power_(); + float get_reactive_power_(); + float get_power_factor_(); + float get_forward_active_energy_(); + float get_reverse_active_energy_(); + float get_frequency_(); + float get_chip_temperature_(); + + sensor::Sensor *freq_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; + sensor::Sensor *forward_active_energy_sensor_{nullptr}; + sensor::Sensor *reverse_active_energy_sensor_{nullptr}; + uint32_t cumulative_forward_active_energy_{0}; + uint32_t cumulative_reverse_active_energy_{0}; + uint16_t gain_metering_{7481}; + uint16_t gain_voltage_{26400}; + uint16_t gain_ct_{31251}; + uint16_t gain_pga_{0x4}; + uint16_t n_line_gain_{0x2}; + int line_freq_{60}; + float meter_constant_{3200.0f}; + uint32_t pl_const_{1429876}; +}; + +} // namespace atm90e26 +} // namespace esphome diff --git a/esphome/components/atm90e26/atm90e26_reg.h b/esphome/components/atm90e26/atm90e26_reg.h new file mode 100644 index 0000000000..0a925f424e --- /dev/null +++ b/esphome/components/atm90e26/atm90e26_reg.h @@ -0,0 +1,70 @@ +#pragma once + +namespace esphome { +namespace atm90e26 { + +/* Status and Special Register */ +static const uint8_t ATM90E26_REGISTER_SOFTRESET = 0x00; // Software Reset +static const uint8_t ATM90E26_REGISTER_SYSSTATUS = 0x01; // System Status +static const uint8_t ATM90E26_REGISTER_FUNCEN = 0x02; // Function Enable +static const uint8_t ATM90E26_REGISTER_SAGTH = 0x03; // Voltage Sag Threshold +static const uint8_t ATM90E26_REGISTER_SMALLPMOD = 0x04; // Small-Power Mode +static const uint8_t ATM90E26_REGISTER_LASTDATA = 0x06; // Last Read/Write SPI/UART Value + +/* Metering Calibration and Configuration Register */ +static const uint8_t ATM90E26_REGISTER_LSB = 0x08; // RMS/Power 16-bit LSB +static const uint8_t ATM90E26_REGISTER_CALSTART = 0x20; // Calibration Start Command +static const uint8_t ATM90E26_REGISTER_PLCONSTH = 0x21; // High Word of PL_Constant +static const uint8_t ATM90E26_REGISTER_PLCONSTL = 0x22; // Low Word of PL_Constant +static const uint8_t ATM90E26_REGISTER_LGAIN = 0x23; // L Line Calibration Gain +static const uint8_t ATM90E26_REGISTER_LPHI = 0x24; // L Line Calibration Angle +static const uint8_t ATM90E26_REGISTER_NGAIN = 0x25; // N Line Calibration Gain +static const uint8_t ATM90E26_REGISTER_NPHI = 0x26; // N Line Calibration Angle +static const uint8_t ATM90E26_REGISTER_PSTARTTH = 0x27; // Active Startup Power Threshold +static const uint8_t ATM90E26_REGISTER_PNOLTH = 0x28; // Active No-Load Power Threshold +static const uint8_t ATM90E26_REGISTER_QSTARTTH = 0x29; // Reactive Startup Power Threshold +static const uint8_t ATM90E26_REGISTER_QNOLTH = 0x2A; // Reactive No-Load Power Threshold +static const uint8_t ATM90E26_REGISTER_MMODE = 0x2B; // Metering Mode Configuration +static const uint8_t ATM90E26_REGISTER_CS1 = 0x2C; // Checksum 1 + +/* Measurement Calibration Register */ +static const uint8_t ATM90E26_REGISTER_ADJSTART = 0x30; // Measurement Calibration Start Command +static const uint8_t ATM90E26_REGISTER_UGAIN = 0x31; // Voltage RMS Gain +static const uint8_t ATM90E26_REGISTER_IGAINL = 0x32; // L Line Current RMS Gain +static const uint8_t ATM90E26_REGISTER_IGAINN = 0x33; // N Line Current RMS Gain +static const uint8_t ATM90E26_REGISTER_UOFFSET = 0x34; // Voltage Offset +static const uint8_t ATM90E26_REGISTER_IOFFSETL = 0x35; // L Line Current Offset +static const uint8_t ATM90E26_REGISTER_IOFFSETN = 0x36; // N Line Current Offse +static const uint8_t ATM90E26_REGISTER_POFFSETL = 0x37; // L Line Active Power Offset +static const uint8_t ATM90E26_REGISTER_QOFFSETL = 0x38; // L Line Reactive Power Offset +static const uint8_t ATM90E26_REGISTER_POFFSETN = 0x39; // N Line Active Power Offset +static const uint8_t ATM90E26_REGISTER_QOFFSETN = 0x3A; // N Line Reactive Power Offset +static const uint8_t ATM90E26_REGISTER_CS2 = 0x3B; // Checksum 2 + +/* Energy Register */ +static const uint8_t ATM90E26_REGISTER_APENERGY = 0x40; // Forward Active Energy +static const uint8_t ATM90E26_REGISTER_ANENERGY = 0x41; // Reverse Active Energy +static const uint8_t ATM90E26_REGISTER_ATENERGY = 0x42; // Absolute Active Energy +static const uint8_t ATM90E26_REGISTER_RPENERGY = 0x43; // Forward (Inductive) Reactive Energy +static const uint8_t ATM90E26_REGISTER_RNENERG = 0x44; // Reverse (Capacitive) Reactive Energy +static const uint8_t ATM90E26_REGISTER_RTENERGY = 0x45; // Absolute Reactive Energy +static const uint8_t ATM90E26_REGISTER_ENSTATUS = 0x46; // Metering Status + +/* Measurement Register */ +static const uint8_t ATM90E26_REGISTER_IRMS = 0x48; // L Line Current RMS +static const uint8_t ATM90E26_REGISTER_URMS = 0x49; // Voltage RMS +static const uint8_t ATM90E26_REGISTER_PMEAN = 0x4A; // L Line Mean Active Power +static const uint8_t ATM90E26_REGISTER_QMEAN = 0x4B; // L Line Mean Reactive Power +static const uint8_t ATM90E26_REGISTER_FREQ = 0x4C; // Voltage Frequency +static const uint8_t ATM90E26_REGISTER_POWERF = 0x4D; // L Line Power Factor +static const uint8_t ATM90E26_REGISTER_PANGLE = 0x4E; // Phase Angle between Voltage and L Line Current +static const uint8_t ATM90E26_REGISTER_SMEAN = 0x4F; // L Line Mean Apparent Power +static const uint8_t ATM90E26_REGISTER_IRMS2 = 0x68; // N Line Current rms +static const uint8_t ATM90E26_REGISTER_PMEAN2 = 0x6A; // N Line Mean Active Power +static const uint8_t ATM90E26_REGISTER_QMEAN2 = 0x6B; // N Line Mean Reactive Power +static const uint8_t ATM90E26_REGISTER_POWERF2 = 0x6D; // N Line Power Factor +static const uint8_t ATM90E26_REGISTER_PANGLE2 = 0x6E; // Phase Angle between Voltage and N Line Current +static const uint8_t ATM90E26_REGISTER_SMEAN2 = 0x6F; // N Line Mean Apparent Power + +} // namespace atm90e26 +} // namespace esphome diff --git a/esphome/components/atm90e26/sensor.py b/esphome/components/atm90e26/sensor.py new file mode 100644 index 0000000000..a0d97ab5ae --- /dev/null +++ b/esphome/components/atm90e26/sensor.py @@ -0,0 +1,157 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, spi +from esphome.const import ( + CONF_ID, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + CONF_CURRENT, + CONF_POWER, + CONF_POWER_FACTOR, + CONF_FREQUENCY, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + ICON_LIGHTBULB, + ICON_CURRENT_AC, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT_HOURS, +) + +CONF_LINE_FREQUENCY = "line_frequency" +CONF_METER_CONSTANT = "meter_constant" +CONF_PL_CONST = "pl_const" +CONF_GAIN_PGA = "gain_pga" +CONF_GAIN_METERING = "gain_metering" +CONF_GAIN_VOLTAGE = "gain_voltage" +CONF_GAIN_CT = "gain_ct" +LINE_FREQS = { + "50HZ": 50, + "60HZ": 60, +} +PGA_GAINS = { + "1X": 0x4, + "4X": 0x0, + "8X": 0x1, + "16X": 0x2, + "24X": 0x3, +} + +atm90e26_ns = cg.esphome_ns.namespace("atm90e26") +ATM90E26Component = atm90e26_ns.class_( + "ATM90E26Component", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ATM90E26Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + icon=ICON_LIGHTBULB, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), + cv.Required(CONF_METER_CONSTANT): cv.positive_float, + cv.Optional(CONF_PL_CONST, default=1429876): cv.uint32_t, + cv.Optional(CONF_GAIN_METERING, default=7481): cv.uint16_t, + cv.Optional(CONF_GAIN_VOLTAGE, default=26400): cv.int_range( + min=0, max=32767 + ), + cv.Optional(CONF_GAIN_CT, default=31251): cv.uint16_t, + cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema()) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + + if CONF_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_VOLTAGE]) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + sens = await sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + sens = await sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) + if CONF_REACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_REACTIVE_POWER]) + cg.add(var.set_reactive_power_sensor(sens)) + if CONF_POWER_FACTOR in config: + sens = await sensor.new_sensor(config[CONF_POWER_FACTOR]) + cg.add(var.set_power_factor_sensor(sens)) + if CONF_FORWARD_ACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_FORWARD_ACTIVE_ENERGY]) + cg.add(var.set_forward_active_energy_sensor(sens)) + if CONF_REVERSE_ACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_REVERSE_ACTIVE_ENERGY]) + cg.add(var.set_reverse_active_energy_sensor(sens)) + if CONF_FREQUENCY in config: + sens = await sensor.new_sensor(config[CONF_FREQUENCY]) + cg.add(var.set_freq_sensor(sens)) + cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) + cg.add(var.set_meter_constant(config[CONF_METER_CONSTANT])) + cg.add(var.set_pl_const(config[CONF_PL_CONST])) + cg.add(var.set_gain_metering(config[CONF_GAIN_METERING])) + cg.add(var.set_gain_voltage(config[CONF_GAIN_VOLTAGE])) + cg.add(var.set_gain_ct(config[CONF_GAIN_CT])) + cg.add(var.set_gain_pga(config[CONF_GAIN_PGA])) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index eaf11c056a..95e35a45f2 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -95,6 +95,14 @@ DEVICE_CLASSES = [ IS_PLATFORM_COMPONENT = True +CONF_TIME_OFF = "time_off" +CONF_TIME_ON = "time_on" + +DEFAULT_DELAY = "1s" +DEFAULT_TIME_OFF = "100ms" +DEFAULT_TIME_ON = "900ms" + + binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor") BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase) BinarySensorInitiallyOff = binary_sensor_ns.class_( @@ -138,47 +146,75 @@ FILTER_REGISTRY = Registry() validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) -@FILTER_REGISTRY.register("invert", InvertFilter, {}) +def register_filter(name, filter_type, schema): + return FILTER_REGISTRY.register(name, filter_type, schema) + + +@register_filter("invert", InvertFilter, {}) async def invert_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id) -@FILTER_REGISTRY.register( - "delayed_on_off", DelayedOnOffFilter, cv.positive_time_period_milliseconds +@register_filter( + "delayed_on_off", + DelayedOnOffFilter, + cv.Any( + cv.templatable(cv.positive_time_period_milliseconds), + cv.Schema( + { + cv.Required(CONF_TIME_ON): cv.templatable( + cv.positive_time_period_milliseconds + ), + cv.Required(CONF_TIME_OFF): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), + msg="'delayed_on_off' filter requires either a delay time to be used for both " + "turn-on and turn-off delays, or two parameters 'time_on' and 'time_off' if " + "different delay times are required.", + ), ) async def delayed_on_off_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config) + var = cg.new_Pvariable(filter_id) await cg.register_component(var, {}) + if isinstance(config, dict): + template_ = await cg.templatable(config[CONF_TIME_ON], [], cg.uint32) + cg.add(var.set_on_delay(template_)) + template_ = await cg.templatable(config[CONF_TIME_OFF], [], cg.uint32) + cg.add(var.set_off_delay(template_)) + else: + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_on_delay(template_)) + cg.add(var.set_off_delay(template_)) return var -@FILTER_REGISTRY.register( - "delayed_on", DelayedOnFilter, cv.positive_time_period_milliseconds +@register_filter( + "delayed_on", DelayedOnFilter, cv.templatable(cv.positive_time_period_milliseconds) ) async def delayed_on_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config) + var = cg.new_Pvariable(filter_id) await cg.register_component(var, {}) + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_delay(template_)) return var -@FILTER_REGISTRY.register( - "delayed_off", DelayedOffFilter, cv.positive_time_period_milliseconds +@register_filter( + "delayed_off", + DelayedOffFilter, + cv.templatable(cv.positive_time_period_milliseconds), ) async def delayed_off_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config) + var = cg.new_Pvariable(filter_id) await cg.register_component(var, {}) + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_delay(template_)) return var -CONF_TIME_OFF = "time_off" -CONF_TIME_ON = "time_on" - -DEFAULT_DELAY = "1s" -DEFAULT_TIME_OFF = "100ms" -DEFAULT_TIME_ON = "900ms" - - -@FILTER_REGISTRY.register( +@register_filter( "autorepeat", AutorepeatFilter, cv.All( @@ -215,7 +251,7 @@ async def autorepeat_filter_to_code(config, filter_id): return var -@FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda) +@register_filter("lambda", LambdaFilter, cv.returning_lambda) async def lambda_filter_to_code(config, filter_id): lambda_ = await cg.process_lambda( config, [(bool, "x")], return_type=cg.optional.template(bool) diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 53c2daf42d..46957383c3 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -26,22 +26,20 @@ void Filter::input(bool value, bool is_initial) { } } -DelayedOnOffFilter::DelayedOnOffFilter(uint32_t delay) : delay_(delay) {} optional DelayedOnOffFilter::new_value(bool value, bool is_initial) { if (value) { - this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(true, is_initial); }); + this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); } else { - this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); }); + this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); } return {}; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {} optional DelayedOnFilter::new_value(bool value, bool is_initial) { if (value) { - this->set_timeout("ON", this->delay_, [this, is_initial]() { this->output(true, is_initial); }); + this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); return {}; } else { this->cancel_timeout("ON"); @@ -51,10 +49,9 @@ optional DelayedOnFilter::new_value(bool value, bool is_initial) { float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -DelayedOffFilter::DelayedOffFilter(uint32_t delay) : delay_(delay) {} optional DelayedOffFilter::new_value(bool value, bool is_initial) { if (!value) { - this->set_timeout("OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); }); + this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); return {}; } else { this->cancel_timeout("OFF"); @@ -114,15 +111,6 @@ LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } -optional UniqueFilter::new_value(bool value, bool is_initial) { - if (this->last_value_.has_value() && *this->last_value_ == value) { - return {}; - } else { - this->last_value_ = value; - return value; - } -} - } // namespace binary_sensor } // namespace esphome diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 64a33f6e34..9514cb3fe2 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -29,38 +30,40 @@ class Filter { class DelayedOnOffFilter : public Filter, public Component { public: - explicit DelayedOnOffFilter(uint32_t delay); - optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; + template void set_on_delay(T delay) { this->on_delay_ = delay; } + template void set_off_delay(T delay) { this->off_delay_ = delay; } + protected: - uint32_t delay_; + TemplatableValue on_delay_{}; + TemplatableValue off_delay_{}; }; class DelayedOnFilter : public Filter, public Component { public: - explicit DelayedOnFilter(uint32_t delay); - optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; + template void set_delay(T delay) { this->delay_ = delay; } + protected: - uint32_t delay_; + TemplatableValue delay_{}; }; class DelayedOffFilter : public Filter, public Component { public: - explicit DelayedOffFilter(uint32_t delay); - optional new_value(bool value, bool is_initial) override; float get_setup_priority() const override; + template void set_delay(T delay) { this->delay_ = delay; } + protected: - uint32_t delay_; + TemplatableValue delay_{}; }; class InvertFilter : public Filter { @@ -105,14 +108,6 @@ class LambdaFilter : public Filter { std::function(bool)> f_; }; -class UniqueFilter : public Filter { - public: - optional new_value(bool value, bool is_initial) override; - - protected: - optional last_value_{}; -}; - } // namespace binary_sensor } // namespace esphome diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py index 8d00012150..3bd082481e 100644 --- a/esphome/components/bme680_bsec/sensor.py +++ b/esphome/components/bme680_bsec/sensor.py @@ -6,8 +6,10 @@ from esphome.const import ( CONF_HUMIDITY, CONF_PRESSURE, CONF_TEMPERATURE, + DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -17,8 +19,6 @@ from esphome.const import ( UNIT_PERCENT, ICON_GAS_CYLINDER, ICON_GAUGE, - ICON_THERMOMETER, - ICON_WATER_PERCENT, ) from . import ( BME680BSECComponent, @@ -35,7 +35,6 @@ CONF_CO2_EQUIVALENT = "co2_equivalent" CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" UNIT_IAQ = "IAQ" ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" -ICON_TEST_TUBE = "mdi:test-tube" TYPES = [ CONF_TEMPERATURE, @@ -53,7 +52,6 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, - icon=ICON_THERMOMETER, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -62,16 +60,14 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, - icon=ICON_GAUGE, accuracy_decimals=1, - device_class=DEVICE_CLASS_PRESSURE, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ).extend( {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, - icon=ICON_WATER_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, @@ -97,14 +93,14 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, - icon=ICON_TEST_TUBE, accuracy_decimals=1, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, - icon=ICON_TEST_TUBE, accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index 3fe0d50f06..6316c77ff4 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -30,7 +30,7 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm if (use_extended_id) { ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); } else { - ESP_LOGD(TAG, "send extended id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); + ESP_LOGD(TAG, "send standard id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); } if (size > CAN_MAX_DATA_LENGTH) size = CAN_MAX_DATA_LENGTH; diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index ff5266e84f..db4a17f6f7 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -21,7 +21,6 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_arduino, cv.only_on(["esp32", "esp8266"]), ) @@ -34,8 +33,9 @@ async def to_code(config): await cg.register_component(var, config) cg.add_define("USE_CAPTIVE_PORTAL") - if CORE.is_esp32: - cg.add_library("DNSServer", None) - cg.add_library("WiFi", None) - if CORE.is_esp8266: - cg.add_library("DNSServer", None) + if CORE.using_arduino: + 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 3bfdea0ab5..74c6606fc0 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "captive_portal.h" #include "esphome/core/log.h" #include "esphome/core/application.h" @@ -46,10 +44,12 @@ void CaptivePortal::start() { this->base_->add_ota_handler(); } +#ifdef USE_ARDUINO this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); this->dns_server_->start(53, "*", (uint32_t) ip); +#endif this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { @@ -67,7 +67,7 @@ void CaptivePortal::start() { void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { if (req->url() == "/") { - AsyncWebServerResponse *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); response->addHeader("Content-Encoding", "gzip"); req->send(response); return; @@ -91,5 +91,3 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo } // namespace captive_portal } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index c2aada171f..df45d40d12 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -1,9 +1,9 @@ #pragma once -#ifdef USE_ARDUINO - #include +#ifdef USE_ARDUINO #include +#endif #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" @@ -18,18 +18,22 @@ class CaptivePortal : public AsyncWebHandler, public Component { CaptivePortal(web_server_base::WebServerBase *base); void setup() override; void dump_config() override; +#ifdef USE_ARDUINO void loop() override { if (this->dns_server_ != nullptr) this->dns_server_->processNextRequest(); } +#endif float get_setup_priority() const override; void start(); bool is_active() const { return this->active_; } void end() { this->active_ = false; this->base_->deinit(); +#ifdef USE_ARDUINO this->dns_server_->stop(); this->dns_server_ = nullptr; +#endif } bool canHandle(AsyncWebServerRequest *request) override { @@ -58,12 +62,12 @@ class CaptivePortal : public AsyncWebHandler, public Component { web_server_base::WebServerBase *base_; bool initialized_{false}; bool active_{false}; +#ifdef USE_ARDUINO std::unique_ptr dns_server_{nullptr}; +#endif }; extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace captive_portal } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 9742b3b19e..1955b5d22c 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -38,7 +38,6 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.polling_component_schema("60s")), - cv.only_on(["esp32", "esp8266"]), ) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 9843fa1c99..8b6a97068b 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -5,6 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/version.h" +#include #ifdef USE_ESP32 @@ -13,6 +14,7 @@ #if ESP_IDF_VERSION_MAJOR >= 4 #include +#include #else #include #endif @@ -20,8 +22,12 @@ #endif // USE_ESP32 #ifdef USE_ARDUINO +#ifdef USE_RP2040 +#include +#else #include #endif +#endif namespace esphome { namespace debug { @@ -33,6 +39,8 @@ static uint32_t get_free_heap() { return ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); +#elif defined(USE_RP2040) + return rp2040.getFreeHeap(); #endif } @@ -61,9 +69,9 @@ void DebugComponent::dump_config() { device_info += ESPHOME_VERSION; this->free_heap_ = get_free_heap(); - ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_); + ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_RP2040) const char *flash_mode; switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) case FM_QIO: @@ -272,6 +280,11 @@ void DebugComponent::dump_config() { reset_reason = ESP.getResetReason().c_str(); #endif +#ifdef USE_RP2040 + ESP_LOGD(TAG, "CPU Frequency: %u", rp2040.f_cpu()); + device_info += "CPU Frequency: " + to_string(rp2040.f_cpu()); +#endif // USE_RP2040 + #ifdef USE_TEXT_SENSOR if (this->device_info_ != nullptr) { if (device_info.length() > 255) @@ -289,7 +302,7 @@ void DebugComponent::loop() { uint32_t new_free_heap = get_free_heap(); if (new_free_heap < this->free_heap_ / 2) { this->free_heap_ = new_free_heap; - ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_); + ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); this->status_momentary_warning("heap", 1000); } diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 0d403f99f0..b7a8508fc8 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -18,10 +18,11 @@ from esphome.core import coroutine_with_priority IS_PLATFORM_COMPONENT = True display_ns = cg.esphome_ns.namespace("display") +Display = display_ns.class_("Display") DisplayBuffer = display_ns.class_("DisplayBuffer") DisplayPage = display_ns.class_("DisplayPage") DisplayPagePtr = DisplayPage.operator("ptr") -DisplayBufferRef = DisplayBuffer.operator("ref") +DisplayRef = Display.operator("ref") DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action) DisplayPageShowNextAction = display_ns.class_( "DisplayPageShowNextAction", automation.Action @@ -96,7 +97,7 @@ async def setup_display_core_(var, config): pages = [] for conf in config[CONF_PAGES]: lambda_ = await cg.process_lambda( - conf[CONF_LAMBDA], [(DisplayBufferRef, "it")], return_type=cg.void + conf[CONF_LAMBDA], [(DisplayRef, "it")], return_type=cg.void ) page = cg.new_Pvariable(conf[CONF_ID], lambda_) pages.append(page) diff --git a/esphome/components/display/animation.h b/esphome/components/display/animation.h deleted file mode 100644 index 38e632ccf0..0000000000 --- a/esphome/components/display/animation.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once -#include "image.h" - -namespace esphome { -namespace display { - -class Animation : public Image { - public: - Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); - - uint32_t get_animation_frame_count() const; - int get_current_frame() const; - void next_frame(); - void prev_frame(); - - /** Selects a specific frame within the animation. - * - * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame. - */ - void set_frame(int frame); - - void set_loop(uint32_t start_frame, uint32_t end_frame, int count); - - protected: - void update_data_start_(); - - const uint8_t *animation_data_start_; - int current_frame_; - uint32_t animation_frame_count_; - uint32_t loop_start_frame_; - uint32_t loop_end_frame_; - int loop_count_; - int loop_current_iteration_; -}; - -} // namespace display -} // namespace esphome diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp new file mode 100644 index 0000000000..410ff58de3 --- /dev/null +++ b/esphome/components/display/display.cpp @@ -0,0 +1,343 @@ +#include "display.h" + +#include + +#include "esphome/core/log.h" + +namespace esphome { +namespace display { + +static const char *const TAG = "display"; + +const Color COLOR_OFF(0, 0, 0, 0); +const Color COLOR_ON(255, 255, 255, 255); + +void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } +void Display::clear() { this->fill(COLOR_OFF); } +void Display::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } +void HOT Display::line(int x1, int y1, int x2, int y2, Color color) { + const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; + const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; + int32_t err = dx + dy; + + while (true) { + this->draw_pixel_at(x1, y1, color); + if (x1 == x2 && y1 == y2) + break; + int32_t e2 = 2 * err; + if (e2 >= dy) { + err += dy; + x1 += sx; + } + if (e2 <= dx) { + err += dx; + y1 += sy; + } + } +} +void HOT Display::horizontal_line(int x, int y, int width, Color color) { + // Future: Could be made more efficient by manipulating buffer directly in certain rotations. + for (int i = x; i < x + width; i++) + this->draw_pixel_at(i, y, color); +} +void HOT Display::vertical_line(int x, int y, int height, Color color) { + // Future: Could be made more efficient by manipulating buffer directly in certain rotations. + for (int i = y; i < y + height; i++) + this->draw_pixel_at(x, i, color); +} +void Display::rectangle(int x1, int y1, int width, int height, Color color) { + this->horizontal_line(x1, y1, width, color); + this->horizontal_line(x1, y1 + height - 1, width, color); + this->vertical_line(x1, y1, height, color); + this->vertical_line(x1 + width - 1, y1, height, color); +} +void Display::filled_rectangle(int x1, int y1, int width, int height, Color color) { + // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. + for (int i = y1; i < y1 + height; i++) { + this->horizontal_line(x1, i, width, color); + } +} +void HOT Display::circle(int center_x, int center_xy, int radius, Color color) { + int dx = -radius; + int dy = 0; + int err = 2 - 2 * radius; + int e2; + + do { + this->draw_pixel_at(center_x - dx, center_xy + dy, color); + this->draw_pixel_at(center_x + dx, center_xy + dy, color); + this->draw_pixel_at(center_x + dx, center_xy - dy, color); + this->draw_pixel_at(center_x - dx, center_xy - dy, color); + e2 = err; + if (e2 < dy) { + err += ++dy * 2 + 1; + if (-dx == dy && e2 <= dx) { + e2 = 0; + } + } + if (e2 > dx) { + err += ++dx * 2 + 1; + } + } while (dx <= 0); +} +void Display::filled_circle(int center_x, int center_y, int radius, Color color) { + int dx = -int32_t(radius); + int dy = 0; + int err = 2 - 2 * radius; + int e2; + + do { + this->draw_pixel_at(center_x - dx, center_y + dy, color); + this->draw_pixel_at(center_x + dx, center_y + dy, color); + this->draw_pixel_at(center_x + dx, center_y - dy, color); + this->draw_pixel_at(center_x - dx, center_y - dy, color); + int hline_width = 2 * (-dx) + 1; + this->horizontal_line(center_x + dx, center_y + dy, hline_width, color); + this->horizontal_line(center_x + dx, center_y - dy, hline_width, color); + e2 = err; + if (e2 < dy) { + err += ++dy * 2 + 1; + if (-dx == dy && e2 <= dx) { + e2 = 0; + } + } + if (e2 > dx) { + err += ++dx * 2 + 1; + } + } while (dx <= 0); +} + +void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) { + int x_start, y_start; + int width, height; + this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); + font->print(x_start, y_start, this, color, text); +} +void Display::vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg) { + char buffer[256]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + if (ret > 0) + this->print(x, y, font, color, align, buffer); +} + +void Display::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { + this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off); +} + +void Display::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) { + auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT))); + auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT))); + + switch (x_align) { + case ImageAlign::RIGHT: + x -= image->get_width(); + break; + case ImageAlign::CENTER_HORIZONTAL: + x -= image->get_width() / 2; + break; + case ImageAlign::LEFT: + default: + break; + } + + switch (y_align) { + case ImageAlign::BOTTOM: + y -= image->get_height(); + break; + case ImageAlign::CENTER_VERTICAL: + y -= image->get_height() / 2; + break; + case ImageAlign::TOP: + default: + break; + } + + image->draw(x, y, this, color_on, color_off); +} + +#ifdef USE_GRAPH +void Display::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); } +void Display::legend(int x, int y, graph::Graph *graph, Color color_on) { graph->draw_legend(this, x, y, color_on); } +#endif // USE_GRAPH + +#ifdef USE_QR_CODE +void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) { + qr_code->draw(this, x, y, color_on, scale); +} +#endif // USE_QR_CODE + +void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, + int *width, int *height) { + int x_offset, baseline; + font->measure(text, width, &x_offset, &baseline, height); + + auto x_align = TextAlign(int(align) & 0x18); + auto y_align = TextAlign(int(align) & 0x07); + + switch (x_align) { + case TextAlign::RIGHT: + *x1 = x - *width; + break; + case TextAlign::CENTER_HORIZONTAL: + *x1 = x - (*width) / 2; + break; + case TextAlign::LEFT: + default: + // LEFT + *x1 = x; + break; + } + + switch (y_align) { + case TextAlign::BOTTOM: + *y1 = y - *height; + break; + case TextAlign::BASELINE: + *y1 = y - baseline; + break; + case TextAlign::CENTER_VERTICAL: + *y1 = y - (*height) / 2; + break; + case TextAlign::TOP: + default: + *y1 = y; + break; + } +} +void Display::print(int x, int y, BaseFont *font, Color color, const char *text) { + this->print(x, y, font, color, TextAlign::TOP_LEFT, text); +} +void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) { + this->print(x, y, font, COLOR_ON, align, text); +} +void Display::print(int x, int y, BaseFont *font, const char *text) { + this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); +} +void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) { + va_list arg; + va_start(arg, format); + this->vprintf_(x, y, font, color, align, format, arg); + va_end(arg); +} +void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { + va_list arg; + va_start(arg, format); + this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); + va_end(arg); +} +void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { + va_list arg; + va_start(arg, format); + this->vprintf_(x, y, font, COLOR_ON, align, format, arg); + va_end(arg); +} +void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { + va_list arg; + va_start(arg, format); + this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg); + va_end(arg); +} +void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } +void Display::set_pages(std::vector pages) { + for (auto *page : pages) + page->set_parent(this); + + for (uint32_t i = 0; i < pages.size() - 1; i++) { + pages[i]->set_next(pages[i + 1]); + pages[i + 1]->set_prev(pages[i]); + } + pages[0]->set_prev(pages[pages.size() - 1]); + pages[pages.size() - 1]->set_next(pages[0]); + this->show_page(pages[0]); +} +void Display::show_page(DisplayPage *page) { + this->previous_page_ = this->page_; + this->page_ = page; + if (this->previous_page_ != this->page_) { + for (auto *t : on_page_change_triggers_) + t->process(this->previous_page_, this->page_); + } +} +void Display::show_next_page() { this->page_->show_next(); } +void Display::show_prev_page() { this->page_->show_prev(); } +void Display::do_update_() { + if (this->auto_clear_enabled_) { + this->clear(); + } + if (this->page_ != nullptr) { + this->page_->get_writer()(*this); + } else if (this->writer_.has_value()) { + (*this->writer_)(*this); + } + // remove all not ended clipping regions + while (is_clipping()) { + end_clipping(); + } +} +void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { + if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) + this->trigger(from, to); +} +void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) { + char buffer[64]; + size_t ret = time.strftime(buffer, sizeof(buffer), format); + if (ret > 0) + this->print(x, y, font, color, align, buffer); +} +void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) { + this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); +} +void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) { + this->strftime(x, y, font, COLOR_ON, align, format, time); +} +void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) { + this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); +} + +void Display::start_clipping(Rect rect) { + if (!this->clipping_rectangle_.empty()) { + Rect r = this->clipping_rectangle_.back(); + rect.shrink(r); + } + this->clipping_rectangle_.push_back(rect); +} +void Display::end_clipping() { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "clear: Clipping is not set."); + } else { + this->clipping_rectangle_.pop_back(); + } +} +void Display::extend_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().extend(add_rect); + } +} +void Display::shrink_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().shrink(add_rect); + } +} +Rect Display::get_clipping() { + if (this->clipping_rectangle_.empty()) { + return Rect(); + } else { + return this->clipping_rectangle_.back(); + } +} + +DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} +void DisplayPage::show() { this->parent_->show_page(this); } +void DisplayPage::show_next() { this->next_->show(); } +void DisplayPage::show_prev() { this->prev_->show(); } +void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; } +void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } +void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } +const display_writer_t &DisplayPage::get_writer() const { return this->writer_; } + +} // namespace display +} // namespace esphome diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h new file mode 100644 index 0000000000..08d8c70e0d --- /dev/null +++ b/esphome/components/display/display.h @@ -0,0 +1,566 @@ +#pragma once + +#include +#include + +#include "rect.h" + +#include "esphome/core/color.h" +#include "esphome/core/automation.h" +#include "esphome/core/time.h" + +#ifdef USE_GRAPH +#include "esphome/components/graph/graph.h" +#endif + +#ifdef USE_QR_CODE +#include "esphome/components/qr_code/qr_code.h" +#endif + +namespace esphome { +namespace display { + +/** TextAlign is used to tell the display class how to position a piece of text. By default + * the coordinates you enter for the print*() functions take the upper left corner of the text + * as the "anchor" point. You can customize this behavior to, for example, make the coordinates + * refer to the *center* of the text. + * + * All text alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis + * these options are allowed: + * + * - LEFT (x-coordinate of anchor point is on left) + * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the text) + * - RIGHT (x-coordinate of anchor point is on right) + * + * For the Y-Axis alignment these options are allowed: + * + * - TOP (y-coordinate of anchor is on the top of the text) + * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the text) + * - BASELINE (y-coordinate of anchor is on the baseline of the text) + * - BOTTOM (y-coordinate of anchor is on the bottom of the text) + * + * These options are then combined to create combined TextAlignment options like: + * - TOP_LEFT (default) + * - CENTER (anchor point is in the middle of the text bounds) + * - ... + */ +enum class TextAlign { + TOP = 0x00, + CENTER_VERTICAL = 0x01, + BASELINE = 0x02, + BOTTOM = 0x04, + + LEFT = 0x00, + CENTER_HORIZONTAL = 0x08, + RIGHT = 0x10, + + TOP_LEFT = TOP | LEFT, + TOP_CENTER = TOP | CENTER_HORIZONTAL, + TOP_RIGHT = TOP | RIGHT, + + CENTER_LEFT = CENTER_VERTICAL | LEFT, + CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL, + CENTER_RIGHT = CENTER_VERTICAL | RIGHT, + + BASELINE_LEFT = BASELINE | LEFT, + BASELINE_CENTER = BASELINE | CENTER_HORIZONTAL, + BASELINE_RIGHT = BASELINE | RIGHT, + + BOTTOM_LEFT = BOTTOM | LEFT, + BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL, + BOTTOM_RIGHT = BOTTOM | RIGHT, +}; + +/** ImageAlign is used to tell the display class how to position a image. By default + * the coordinates you enter for the image() functions take the upper left corner of the image + * as the "anchor" point. You can customize this behavior to, for example, make the coordinates + * refer to the *center* of the image. + * + * All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis + * these options are allowed: + * + * - LEFT (x-coordinate of anchor point is on left) + * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image) + * - RIGHT (x-coordinate of anchor point is on right) + * + * For the Y-Axis alignment these options are allowed: + * + * - TOP (y-coordinate of anchor is on the top of the image) + * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image) + * - BOTTOM (y-coordinate of anchor is on the bottom of the image) + * + * These options are then combined to create combined TextAlignment options like: + * - TOP_LEFT (default) + * - CENTER (anchor point is in the middle of the image bounds) + * - ... + */ +enum class ImageAlign { + TOP = 0x00, + CENTER_VERTICAL = 0x01, + BOTTOM = 0x02, + + LEFT = 0x00, + CENTER_HORIZONTAL = 0x04, + RIGHT = 0x08, + + TOP_LEFT = TOP | LEFT, + TOP_CENTER = TOP | CENTER_HORIZONTAL, + TOP_RIGHT = TOP | RIGHT, + + CENTER_LEFT = CENTER_VERTICAL | LEFT, + CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL, + CENTER_RIGHT = CENTER_VERTICAL | RIGHT, + + BOTTOM_LEFT = BOTTOM | LEFT, + BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL, + BOTTOM_RIGHT = BOTTOM | RIGHT, + + HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT, + VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM +}; + +enum DisplayType { + DISPLAY_TYPE_BINARY = 1, + DISPLAY_TYPE_GRAYSCALE = 2, + DISPLAY_TYPE_COLOR = 3, +}; + +enum DisplayRotation { + DISPLAY_ROTATION_0_DEGREES = 0, + DISPLAY_ROTATION_90_DEGREES = 90, + DISPLAY_ROTATION_180_DEGREES = 180, + DISPLAY_ROTATION_270_DEGREES = 270, +}; + +class Display; +class DisplayPage; +class DisplayOnPageChangeTrigger; + +using display_writer_t = std::function; + +#define LOG_DISPLAY(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, prefix type); \ + ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, (obj)->rotation_); \ + ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \ + } + +/// Turn the pixel OFF. +extern const Color COLOR_OFF; +/// Turn the pixel ON. +extern const Color COLOR_ON; + +class BaseImage { + public: + virtual void draw(int x, int y, Display *display, Color color_on, Color color_off) = 0; + virtual int get_width() const = 0; + virtual int get_height() const = 0; +}; + +class BaseFont { + public: + virtual void print(int x, int y, Display *display, Color color, const char *text) = 0; + virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; +}; + +class Display { + public: + /// Fill the entire screen with the given color. + virtual void fill(Color color); + /// Clear the entire screen by filling it with OFF pixels. + void clear(); + + /// Get the width of the image in pixels with rotation applied. + virtual int get_width() = 0; + /// Get the height of the image in pixels with rotation applied. + virtual int get_height() = 0; + + /// Set a single pixel at the specified coordinates to default color. + inline void draw_pixel_at(int x, int y) { this->draw_pixel_at(x, y, COLOR_ON); } + + /// Set a single pixel at the specified coordinates to the given color. + virtual void draw_pixel_at(int x, int y, Color color) = 0; + + /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. + void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON); + + /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color. + void horizontal_line(int x, int y, int width, Color color = COLOR_ON); + + /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color. + void vertical_line(int x, int y, int height, Color color = COLOR_ON); + + /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at + /// [x1+width,y1+height]. + void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); + + /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height]. + void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); + + /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color. + void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON); + + /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. + void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON); + + /** Print `text` with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param align The alignment of the text. + * @param text The text to draw. + */ + void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text); + + /** Print `text` with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param text The text to draw. + */ + void print(int x, int y, BaseFont *font, Color color, const char *text); + + /** Print `text` with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param align The alignment of the text. + * @param text The text to draw. + */ + void print(int x, int y, BaseFont *font, TextAlign align, const char *text); + + /** Print `text` with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param text The text to draw. + */ + void print(int x, int y, BaseFont *font, const char *text); + + /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param align The alignment of the text. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) + __attribute__((format(printf, 7, 8))); + + /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); + + /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param align The alignment of the text. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) + __attribute__((format(printf, 6, 7))); + + /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param format The format to use. + * @param ... The arguments to use for the text formatting. + */ + void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6))); + + /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param align The alignment of the text. + * @param format The strftime format to use. + * @param time The time to format. + */ + void strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) + __attribute__((format(strftime, 7, 0))); + + /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param color The color to draw the text with. + * @param format The strftime format to use. + * @param time The time to format. + */ + void strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) + __attribute__((format(strftime, 6, 0))); + + /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. + * + * @param x The x coordinate of the text alignment anchor point. + * @param y The y coordinate of the text alignment anchor point. + * @param font The font to draw the text with. + * @param align The alignment of the text. + * @param format The strftime format to use. + * @param time The time to format. + */ + void strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) + __attribute__((format(strftime, 6, 0))); + + /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param font The font to draw the text with. + * @param format The strftime format to use. + * @param time The time to format. + */ + void strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0))); + + /** Draw the `image` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param image The image to draw. + * @param color_on The color to replace in binary images for the on bits. + * @param color_off The color to replace in binary images for the off bits. + */ + void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); + + /** Draw the `image` at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param image The image to draw. + * @param align The alignment of the image. + * @param color_on The color to replace in binary images for the on bits. + * @param color_off The color to replace in binary images for the off bits. + */ + void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); + +#ifdef USE_GRAPH + /** Draw the `graph` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param graph The graph id to draw + * @param color_on The color to replace in binary images for the on bits. + */ + void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); + + /** Draw the `legend` for graph with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param graph The graph id for which the legend applies to + * @param graph The graph id for which the legend applies to + * @param graph The graph id for which the legend applies to + * @param name_font The font used for the trace name + * @param value_font The font used for the trace value and units + * @param color_on The color of the border + */ + void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); +#endif // USE_GRAPH + +#ifdef USE_QR_CODE + /** Draw the `qr_code` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param qr_code The qr_code to draw + * @param color_on The color to replace in binary images for the on bits. + */ + void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); +#endif + + /** Get the text bounds of the given string. + * + * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. + * @param y The y coordinate to place the string at, can be 0 if only interested in dimensions. + * @param text The text to measure. + * @param font The font to measure the text bounds with. + * @param align The alignment of the text. Set to TextAlign::TOP_LEFT if only interested in dimensions. + * @param x1 A pointer to store the returned x coordinate of the upper left corner in. + * @param y1 A pointer to store the returned y coordinate of the upper left corner in. + * @param width A pointer to store the returned text width in. + * @param height A pointer to store the returned text height in. + */ + void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width, + int *height); + + /// Internal method to set the display writer lambda. + void set_writer(display_writer_t &&writer); + + void show_page(DisplayPage *page); + void show_next_page(); + void show_prev_page(); + + void set_pages(std::vector pages); + + const DisplayPage *get_active_page() const { return this->page_; } + + void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); } + + /// 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; } + + DisplayRotation get_rotation() const { return this->rotation_; } + + /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays, + * returns the type the display is currently configured to. + */ + virtual DisplayType get_display_type() = 0; + + /** Set the clipping rectangle for further drawing + * + * @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) + * + * return true if success, false if error + */ + void start_clipping(Rect rect); + void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + start_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** Add a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ + void extend_clipping(Rect rect); + void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + this->extend_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** substract a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ + void shrink_clipping(Rect rect); + void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { + this->shrink_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** Reset the invalidation region + */ + void end_clipping(); + + /** Get the current the clipping rectangle + * + * return rect for active clipping region + */ + Rect get_clipping(); + + bool is_clipping() const { return !this->clipping_rectangle_.empty(); } + + protected: + void vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg); + + void do_update_(); + + DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; + optional writer_{}; + DisplayPage *page_{nullptr}; + DisplayPage *previous_page_{nullptr}; + std::vector on_page_change_triggers_; + bool auto_clear_enabled_{true}; + std::vector clipping_rectangle_; +}; + +class DisplayPage { + public: + DisplayPage(display_writer_t writer); + void show(); + void show_next(); + void show_prev(); + void set_parent(Display *parent); + void set_prev(DisplayPage *prev); + void set_next(DisplayPage *next); + const display_writer_t &get_writer() const; + + protected: + Display *parent_; + display_writer_t writer_; + DisplayPage *prev_{nullptr}; + DisplayPage *next_{nullptr}; +}; + +template class DisplayPageShowAction : public Action { + public: + TEMPLATABLE_VALUE(DisplayPage *, page) + + void play(Ts... x) override { + auto *page = this->page_.value(x...); + if (page != nullptr) { + page->show(); + } + } +}; + +template class DisplayPageShowNextAction : public Action { + public: + DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {} + + void play(Ts... x) override { this->buffer_->show_next_page(); } + + Display *buffer_; +}; + +template class DisplayPageShowPrevAction : public Action { + public: + DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {} + + void play(Ts... x) override { this->buffer_->show_prev_page(); } + + Display *buffer_; +}; + +template class DisplayIsDisplayingPageCondition : public Condition { + public: + DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {} + + void set_page(DisplayPage *page) { this->page_ = page; } + bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } + + protected: + Display *parent_; + DisplayPage *page_; +}; + +class DisplayOnPageChangeTrigger : public Trigger { + public: + explicit DisplayOnPageChangeTrigger(Display *parent) { parent->add_on_page_change_trigger(this); } + void process(DisplayPage *from, DisplayPage *to); + void set_from(DisplayPage *p) { this->from_ = p; } + void set_to(DisplayPage *p) { this->to_ = p; } + + protected: + DisplayPage *from_{nullptr}; + DisplayPage *to_{nullptr}; +}; + +} // namespace display +} // namespace esphome diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 791f7350e6..3af1b63e01 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -1,111 +1,15 @@ #include "display_buffer.h" #include -#include "esphome/core/application.h" -#include "esphome/core/color.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" -#include "animation.h" -#include "image.h" -#include "font.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" namespace esphome { namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 0); -const Color COLOR_ON(255, 255, 255, 255); - -void Rect::expand(int16_t horizontal, int16_t vertical) { - if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) { - this->x = this->x - horizontal; - this->y = this->y - vertical; - this->w = this->w + (2 * horizontal); - this->h = this->h + (2 * vertical); - } -} - -void Rect::extend(Rect rect) { - if (!this->is_set()) { - this->x = rect.x; - this->y = rect.y; - this->w = rect.w; - this->h = rect.h; - } else { - if (this->x > rect.x) { - this->w = this->w + (this->x - rect.x); - this->x = rect.x; - } - if (this->y > rect.y) { - this->h = this->h + (this->y - rect.y); - this->y = rect.y; - } - if (this->x2() < rect.x2()) { - this->w = rect.x2() - this->x; - } - if (this->y2() < rect.y2()) { - this->h = rect.y2() - this->y; - } - } -} -void Rect::shrink(Rect rect) { - if (!this->inside(rect)) { - (*this) = Rect(); - } else { - if (this->x2() > rect.x2()) { - this->w = rect.x2() - this->x; - } - if (this->x < rect.x) { - this->w = this->w + (this->x - rect.x); - this->x = rect.x; - } - if (this->y2() > rect.y2()) { - this->h = rect.y2() - this->y; - } - if (this->y < rect.y) { - this->h = this->h + (this->y - rect.y); - this->y = rect.y; - } - } -} - -bool Rect::equal(Rect rect) { - return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h); -} - -bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT - if (!this->is_set()) { - return true; - } - if (absolute) { - return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2())); - } else { - return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h)); - } -} - -bool Rect::inside(Rect rect, bool absolute) { - if (!this->is_set() || !rect.is_set()) { - return true; - } - if (absolute) { - return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); - } else { - return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); - } -} - -void Rect::info(const std::string &prefix) { - if (this->is_set()) { - ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(), - this->y2()); - } else - ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); -} - void DisplayBuffer::init_internal_(uint32_t buffer_length) { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->buffer_ = allocator.allocate(buffer_length); @@ -116,8 +20,6 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) { this->clear(); } -void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } -void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { switch (this->rotation_) { case DISPLAY_ROTATION_90_DEGREES: @@ -129,6 +31,7 @@ int DisplayBuffer::get_width() { return this->get_width_internal(); } } + int DisplayBuffer::get_height() { switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: @@ -140,7 +43,7 @@ int DisplayBuffer::get_height() { return this->get_width_internal(); } } -void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } + void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { if (!this->get_clipping().inside(x, y)) return; // NOLINT @@ -164,372 +67,6 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { this->draw_absolute_pixel_internal(x, y, color); App.feed_wdt(); } -void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, Color color) { - const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; - const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; - int32_t err = dx + dy; - - while (true) { - this->draw_pixel_at(x1, y1, color); - if (x1 == x2 && y1 == y2) - break; - int32_t e2 = 2 * err; - if (e2 >= dy) { - err += dy; - x1 += sx; - } - if (e2 <= dx) { - err += dx; - y1 += sy; - } - } -} -void HOT DisplayBuffer::horizontal_line(int x, int y, int width, Color color) { - // Future: Could be made more efficient by manipulating buffer directly in certain rotations. - for (int i = x; i < x + width; i++) - this->draw_pixel_at(i, y, color); -} -void HOT DisplayBuffer::vertical_line(int x, int y, int height, Color color) { - // Future: Could be made more efficient by manipulating buffer directly in certain rotations. - for (int i = y; i < y + height; i++) - this->draw_pixel_at(x, i, color); -} -void DisplayBuffer::rectangle(int x1, int y1, int width, int height, Color color) { - this->horizontal_line(x1, y1, width, color); - this->horizontal_line(x1, y1 + height - 1, width, color); - this->vertical_line(x1, y1, height, color); - this->vertical_line(x1 + width - 1, y1, height, color); -} -void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, Color color) { - // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. - for (int i = y1; i < y1 + height; i++) { - this->horizontal_line(x1, i, width, color); - } -} -void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, Color color) { - int dx = -radius; - int dy = 0; - int err = 2 - 2 * radius; - int e2; - - do { - this->draw_pixel_at(center_x - dx, center_xy + dy, color); - this->draw_pixel_at(center_x + dx, center_xy + dy, color); - this->draw_pixel_at(center_x + dx, center_xy - dy, color); - this->draw_pixel_at(center_x - dx, center_xy - dy, color); - e2 = err; - if (e2 < dy) { - err += ++dy * 2 + 1; - if (-dx == dy && e2 <= dx) { - e2 = 0; - } - } - if (e2 > dx) { - err += ++dx * 2 + 1; - } - } while (dx <= 0); -} -void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color color) { - int dx = -int32_t(radius); - int dy = 0; - int err = 2 - 2 * radius; - int e2; - - do { - this->draw_pixel_at(center_x - dx, center_y + dy, color); - this->draw_pixel_at(center_x + dx, center_y + dy, color); - this->draw_pixel_at(center_x + dx, center_y - dy, color); - this->draw_pixel_at(center_x - dx, center_y - dy, color); - int hline_width = 2 * (-dx) + 1; - this->horizontal_line(center_x + dx, center_y + dy, hline_width, color); - this->horizontal_line(center_x + dx, center_y - dy, hline_width, color); - e2 = err; - if (e2 < dy) { - err += ++dy * 2 + 1; - if (-dx == dy && e2 <= dx) { - e2 = 0; - } - } - if (e2 > dx) { - err += ++dx * 2 + 1; - } - } while (dx <= 0); -} - -void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) { - int x_start, y_start; - int width, height; - this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); - - int i = 0; - int x_at = x_start; - while (text[i] != '\0') { - int match_length; - int glyph_n = font->match_next_glyph(text + i, &match_length); - if (glyph_n < 0) { - // Unknown char, skip - ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); - if (!font->get_glyphs().empty()) { - uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width; - for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) { - for (int glyph_y = 0; glyph_y < height; glyph_y++) - this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); - } - x_at += glyph_width; - } - - i++; - continue; - } - - const Glyph &glyph = font->get_glyphs()[glyph_n]; - int scan_x1, scan_y1, scan_width, scan_height; - glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); - - { - const int glyph_x_max = scan_x1 + scan_width; - const int glyph_y_max = scan_y1 + scan_height; - for (int glyph_x = scan_x1; glyph_x < glyph_x_max; glyph_x++) { - for (int glyph_y = scan_y1; glyph_y < glyph_y_max; glyph_y++) { - if (glyph.get_pixel(glyph_x, glyph_y)) { - this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); - } - } - } - } - - x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; - - i += match_length; - } -} -void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) { - char buffer[256]; - int ret = vsnprintf(buffer, sizeof(buffer), format, arg); - if (ret > 0) - this->print(x, y, font, color, align, buffer); -} - -void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { - this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off); -} - -void DisplayBuffer::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) { - auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT))); - auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT))); - - switch (x_align) { - case ImageAlign::RIGHT: - x -= image->get_width(); - break; - case ImageAlign::CENTER_HORIZONTAL: - x -= image->get_width() / 2; - break; - case ImageAlign::LEFT: - default: - break; - } - - switch (y_align) { - case ImageAlign::BOTTOM: - y -= image->get_height(); - break; - case ImageAlign::CENTER_VERTICAL: - y -= image->get_height() / 2; - break; - case ImageAlign::TOP: - default: - break; - } - - image->draw(x, y, this, color_on, color_off); -} - -#ifdef USE_GRAPH -void DisplayBuffer::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); } -void DisplayBuffer::legend(int x, int y, graph::Graph *graph, Color color_on) { - graph->draw_legend(this, x, y, color_on); -} -#endif // USE_GRAPH - -#ifdef USE_QR_CODE -void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) { - qr_code->draw(this, x, y, color_on, scale); -} -#endif // USE_QR_CODE - -void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, - int *width, int *height) { - int x_offset, baseline; - font->measure(text, width, &x_offset, &baseline, height); - - auto x_align = TextAlign(int(align) & 0x18); - auto y_align = TextAlign(int(align) & 0x07); - - switch (x_align) { - case TextAlign::RIGHT: - *x1 = x - *width; - break; - case TextAlign::CENTER_HORIZONTAL: - *x1 = x - (*width) / 2; - break; - case TextAlign::LEFT: - default: - // LEFT - *x1 = x; - break; - } - - switch (y_align) { - case TextAlign::BOTTOM: - *y1 = y - *height; - break; - case TextAlign::BASELINE: - *y1 = y - baseline; - break; - case TextAlign::CENTER_VERTICAL: - *y1 = y - (*height) / 2; - break; - case TextAlign::TOP: - default: - *y1 = y; - break; - } -} -void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) { - this->print(x, y, font, color, TextAlign::TOP_LEFT, text); -} -void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) { - this->print(x, y, font, COLOR_ON, align, text); -} -void DisplayBuffer::print(int x, int y, Font *font, const char *text) { - this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); -} -void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) { - va_list arg; - va_start(arg, format); - this->vprintf_(x, y, font, color, align, format, arg); - va_end(arg); -} -void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) { - va_list arg; - va_start(arg, format); - this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); - va_end(arg); -} -void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char *format, ...) { - va_list arg; - va_start(arg, format); - this->vprintf_(x, y, font, COLOR_ON, align, format, arg); - va_end(arg); -} -void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) { - va_list arg; - va_start(arg, format); - this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg); - va_end(arg); -} -void DisplayBuffer::set_writer(display_writer_t &&writer) { this->writer_ = writer; } -void DisplayBuffer::set_pages(std::vector pages) { - for (auto *page : pages) - page->set_parent(this); - - for (uint32_t i = 0; i < pages.size() - 1; i++) { - pages[i]->set_next(pages[i + 1]); - pages[i + 1]->set_prev(pages[i]); - } - pages[0]->set_prev(pages[pages.size() - 1]); - pages[pages.size() - 1]->set_next(pages[0]); - this->show_page(pages[0]); -} -void DisplayBuffer::show_page(DisplayPage *page) { - this->previous_page_ = this->page_; - this->page_ = page; - if (this->previous_page_ != this->page_) { - for (auto *t : on_page_change_triggers_) - t->process(this->previous_page_, this->page_); - } -} -void DisplayBuffer::show_next_page() { this->page_->show_next(); } -void DisplayBuffer::show_prev_page() { this->page_->show_prev(); } -void DisplayBuffer::do_update_() { - if (this->auto_clear_enabled_) { - this->clear(); - } - if (this->page_ != nullptr) { - this->page_->get_writer()(*this); - } else if (this->writer_.has_value()) { - (*this->writer_)(*this); - } - // remove all not ended clipping regions - while (is_clipping()) { - end_clipping(); - } -} -void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { - if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) - this->trigger(from, to); -} -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) { - char buffer[64]; - size_t ret = time.strftime(buffer, sizeof(buffer), format); - if (ret > 0) - this->print(x, y, font, color, align, buffer); -} -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) { - this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); -} -void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) { - this->strftime(x, y, font, COLOR_ON, align, format, time); -} -void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, ESPTime time) { - this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); -} - -void DisplayBuffer::start_clipping(Rect rect) { - if (!this->clipping_rectangle_.empty()) { - Rect r = this->clipping_rectangle_.back(); - rect.shrink(r); - } - this->clipping_rectangle_.push_back(rect); -} -void DisplayBuffer::end_clipping() { - if (this->clipping_rectangle_.empty()) { - ESP_LOGE(TAG, "clear: Clipping is not set."); - } else { - this->clipping_rectangle_.pop_back(); - } -} -void DisplayBuffer::extend_clipping(Rect add_rect) { - if (this->clipping_rectangle_.empty()) { - ESP_LOGE(TAG, "add: Clipping is not set."); - } else { - this->clipping_rectangle_.back().extend(add_rect); - } -} -void DisplayBuffer::shrink_clipping(Rect add_rect) { - if (this->clipping_rectangle_.empty()) { - ESP_LOGE(TAG, "add: Clipping is not set."); - } else { - this->clipping_rectangle_.back().shrink(add_rect); - } -} -Rect DisplayBuffer::get_clipping() { - if (this->clipping_rectangle_.empty()) { - return Rect(); - } else { - return this->clipping_rectangle_.back(); - } -} - -DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} -void DisplayPage::show() { this->parent_->show_page(this); } -void DisplayPage::show_next() { this->next_->show(); } -void DisplayPage::show_prev() { this->prev_->show(); } -void DisplayPage::set_parent(DisplayBuffer *parent) { this->parent_ = parent; } -void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } -void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } -const display_writer_t &DisplayPage::get_writer() const { return this->writer_; } } // namespace display } // namespace esphome diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 652039517f..869d97613a 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -2,579 +2,35 @@ #include #include + +#include "display.h" #include "display_color_utils.h" -#include "esphome/core/automation.h" + #include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/time.h" - -#ifdef USE_GRAPH -#include "esphome/components/graph/graph.h" -#endif - -#ifdef USE_QR_CODE -#include "esphome/components/qr_code/qr_code.h" -#endif - -#include "animation.h" -#include "font.h" -#include "image.h" namespace esphome { namespace display { -/** TextAlign is used to tell the display class how to position a piece of text. By default - * the coordinates you enter for the print*() functions take the upper left corner of the text - * as the "anchor" point. You can customize this behavior to, for example, make the coordinates - * refer to the *center* of the text. - * - * All text alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis - * these options are allowed: - * - * - LEFT (x-coordinate of anchor point is on left) - * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the text) - * - RIGHT (x-coordinate of anchor point is on right) - * - * For the Y-Axis alignment these options are allowed: - * - * - TOP (y-coordinate of anchor is on the top of the text) - * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the text) - * - BASELINE (y-coordinate of anchor is on the baseline of the text) - * - BOTTOM (y-coordinate of anchor is on the bottom of the text) - * - * These options are then combined to create combined TextAlignment options like: - * - TOP_LEFT (default) - * - CENTER (anchor point is in the middle of the text bounds) - * - ... - */ -enum class TextAlign { - TOP = 0x00, - CENTER_VERTICAL = 0x01, - BASELINE = 0x02, - BOTTOM = 0x04, - - LEFT = 0x00, - CENTER_HORIZONTAL = 0x08, - RIGHT = 0x10, - - TOP_LEFT = TOP | LEFT, - TOP_CENTER = TOP | CENTER_HORIZONTAL, - TOP_RIGHT = TOP | RIGHT, - - CENTER_LEFT = CENTER_VERTICAL | LEFT, - CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL, - CENTER_RIGHT = CENTER_VERTICAL | RIGHT, - - BASELINE_LEFT = BASELINE | LEFT, - BASELINE_CENTER = BASELINE | CENTER_HORIZONTAL, - BASELINE_RIGHT = BASELINE | RIGHT, - - BOTTOM_LEFT = BOTTOM | LEFT, - BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL, - BOTTOM_RIGHT = BOTTOM | RIGHT, -}; - -/** ImageAlign is used to tell the display class how to position a image. By default - * the coordinates you enter for the image() functions take the upper left corner of the image - * as the "anchor" point. You can customize this behavior to, for example, make the coordinates - * refer to the *center* of the image. - * - * All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis - * these options are allowed: - * - * - LEFT (x-coordinate of anchor point is on left) - * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image) - * - RIGHT (x-coordinate of anchor point is on right) - * - * For the Y-Axis alignment these options are allowed: - * - * - TOP (y-coordinate of anchor is on the top of the image) - * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image) - * - BOTTOM (y-coordinate of anchor is on the bottom of the image) - * - * These options are then combined to create combined TextAlignment options like: - * - TOP_LEFT (default) - * - CENTER (anchor point is in the middle of the image bounds) - * - ... - */ -enum class ImageAlign { - TOP = 0x00, - CENTER_VERTICAL = 0x01, - BOTTOM = 0x02, - - LEFT = 0x00, - CENTER_HORIZONTAL = 0x04, - RIGHT = 0x08, - - TOP_LEFT = TOP | LEFT, - TOP_CENTER = TOP | CENTER_HORIZONTAL, - TOP_RIGHT = TOP | RIGHT, - - CENTER_LEFT = CENTER_VERTICAL | LEFT, - CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL, - CENTER_RIGHT = CENTER_VERTICAL | RIGHT, - - BOTTOM_LEFT = BOTTOM | LEFT, - BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL, - BOTTOM_RIGHT = BOTTOM | RIGHT, - - HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT, - VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM -}; - -enum DisplayType { - DISPLAY_TYPE_BINARY = 1, - DISPLAY_TYPE_GRAYSCALE = 2, - DISPLAY_TYPE_COLOR = 3, -}; - -enum DisplayRotation { - DISPLAY_ROTATION_0_DEGREES = 0, - DISPLAY_ROTATION_90_DEGREES = 90, - DISPLAY_ROTATION_180_DEGREES = 180, - DISPLAY_ROTATION_270_DEGREES = 270, -}; - -static const int16_t VALUE_NO_SET = 32766; - -class Rect { +class DisplayBuffer : public Display { public: - int16_t x; ///< X coordinate of corner - int16_t y; ///< Y coordinate of corner - int16_t w; ///< Width of region - int16_t h; ///< Height of region - - Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT - inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {} - inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner - inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner - - inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); } - - void expand(int16_t horizontal, int16_t vertical); - - void extend(Rect rect); - void shrink(Rect rect); - - bool inside(Rect rect, bool absolute = true); - bool inside(int16_t test_x, int16_t test_y, bool absolute = true); - bool equal(Rect rect); - void info(const std::string &prefix = "rect info:"); -}; - -class DisplayBuffer; -class DisplayPage; -class DisplayOnPageChangeTrigger; - -using display_writer_t = std::function; - -#define LOG_DISPLAY(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type); \ - ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, (obj)->rotation_); \ - ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \ - } - -class DisplayBuffer { - public: - /// Fill the entire screen with the given color. - virtual void fill(Color color); - /// Clear the entire screen by filling it with OFF pixels. - void clear(); - /// Get the width of the image in pixels with rotation applied. - int get_width(); + int get_width() override; /// Get the height of the image in pixels with rotation applied. - int get_height(); + int get_height() override; /// Set a single pixel at the specified coordinates to the given color. - void draw_pixel_at(int x, int y, Color color = COLOR_ON); - - /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. - void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON); - - /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color. - void horizontal_line(int x, int y, int width, Color color = COLOR_ON); - - /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color. - void vertical_line(int x, int y, int height, Color color = COLOR_ON); - - /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at - /// [x1+width,y1+height]. - void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); - - /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height]. - void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); - - /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color. - void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON); - - /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. - void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON); - - /** Print `text` with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param align The alignment of the text. - * @param text The text to draw. - */ - void print(int x, int y, Font *font, Color color, TextAlign align, const char *text); - - /** Print `text` with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param text The text to draw. - */ - void print(int x, int y, Font *font, Color color, const char *text); - - /** Print `text` with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param align The alignment of the text. - * @param text The text to draw. - */ - void print(int x, int y, Font *font, TextAlign align, const char *text); - - /** Print `text` with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param text The text to draw. - */ - void print(int x, int y, Font *font, const char *text); - - /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param align The alignment of the text. - * @param format The format to use. - * @param ... The arguments to use for the text formatting. - */ - void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) - __attribute__((format(printf, 7, 8))); - - /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param format The format to use. - * @param ... The arguments to use for the text formatting. - */ - void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); - - /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param align The alignment of the text. - * @param format The format to use. - * @param ... The arguments to use for the text formatting. - */ - void printf(int x, int y, Font *font, TextAlign align, const char *format, ...) __attribute__((format(printf, 6, 7))); - - /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param format The format to use. - * @param ... The arguments to use for the text formatting. - */ - void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6))); - - /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param align The alignment of the text. - * @param format The strftime format to use. - * @param time The time to format. - */ - void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) - __attribute__((format(strftime, 7, 0))); - - /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param color The color to draw the text with. - * @param format The strftime format to use. - * @param time The time to format. - */ - void strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) - __attribute__((format(strftime, 6, 0))); - - /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. - * - * @param x The x coordinate of the text alignment anchor point. - * @param y The y coordinate of the text alignment anchor point. - * @param font The font to draw the text with. - * @param align The alignment of the text. - * @param format The strftime format to use. - * @param time The time to format. - */ - void strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) - __attribute__((format(strftime, 6, 0))); - - /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param font The font to draw the text with. - * @param format The strftime format to use. - * @param time The time to format. - */ - void strftime(int x, int y, Font *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0))); - - /** Draw the `image` with the top-left corner at [x,y] to the screen. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param image The image to draw. - * @param color_on The color to replace in binary images for the on bits. - * @param color_off The color to replace in binary images for the off bits. - */ - void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); - - /** Draw the `image` at [x,y] to the screen. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param image The image to draw. - * @param align The alignment of the image. - * @param color_on The color to replace in binary images for the on bits. - * @param color_off The color to replace in binary images for the off bits. - */ - void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); - -#ifdef USE_GRAPH - /** Draw the `graph` with the top-left corner at [x,y] to the screen. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param graph The graph id to draw - * @param color_on The color to replace in binary images for the on bits. - */ - void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); - - /** Draw the `legend` for graph with the top-left corner at [x,y] to the screen. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param graph The graph id for which the legend applies to - * @param graph The graph id for which the legend applies to - * @param graph The graph id for which the legend applies to - * @param name_font The font used for the trace name - * @param value_font The font used for the trace value and units - * @param color_on The color of the border - */ - void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); -#endif // USE_GRAPH - -#ifdef USE_QR_CODE - /** Draw the `qr_code` with the top-left corner at [x,y] to the screen. - * - * @param x The x coordinate of the upper left corner. - * @param y The y coordinate of the upper left corner. - * @param qr_code The qr_code to draw - * @param color_on The color to replace in binary images for the on bits. - */ - void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); -#endif - - /** Get the text bounds of the given string. - * - * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. - * @param y The y coordinate to place the string at, can be 0 if only interested in dimensions. - * @param text The text to measure. - * @param font The font to measure the text bounds with. - * @param align The alignment of the text. Set to TextAlign::TOP_LEFT if only interested in dimensions. - * @param x1 A pointer to store the returned x coordinate of the upper left corner in. - * @param y1 A pointer to store the returned y coordinate of the upper left corner in. - * @param width A pointer to store the returned text width in. - * @param height A pointer to store the returned text height in. - */ - void get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width, - int *height); - - /// Internal method to set the display writer lambda. - void set_writer(display_writer_t &&writer); - - void show_page(DisplayPage *page); - void show_next_page(); - void show_prev_page(); - - void set_pages(std::vector pages); - - const DisplayPage *get_active_page() const { return this->page_; } - - void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); } - - /// 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; } + void draw_pixel_at(int x, int y, Color color) override; virtual int get_height_internal() = 0; virtual int get_width_internal() = 0; - DisplayRotation get_rotation() const { return this->rotation_; } - - /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays, - * returns the type the display is currently configured to. - */ - virtual DisplayType get_display_type() = 0; - - /** Set the clipping rectangle for further drawing - * - * @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) - * - * return true if success, false if error - */ - void start_clipping(Rect rect); - void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { - start_clipping(Rect(left, top, right - left, bottom - top)); - }; - - /** Add a rectangular region to the invalidation region - * - This is usually called when an element has been modified - * - * @param[in] rect: Rectangle to add to the invalidation region - */ - void extend_clipping(Rect rect); - void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { - this->extend_clipping(Rect(left, top, right - left, bottom - top)); - }; - - /** substract a rectangular region to the invalidation region - * - This is usually called when an element has been modified - * - * @param[in] rect: Rectangle to add to the invalidation region - */ - void shrink_clipping(Rect rect); - void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { - this->shrink_clipping(Rect(left, top, right - left, bottom - top)); - }; - - /** Reset the invalidation region - */ - void end_clipping(); - - /** Get the current the clipping rectangle - * - * return rect for active clipping region - */ - Rect get_clipping(); - - bool is_clipping() const { return !this->clipping_rectangle_.empty(); } protected: - void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); - virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0; void init_internal_(uint32_t buffer_length); - void do_update_(); - uint8_t *buffer_{nullptr}; - DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; - optional writer_{}; - DisplayPage *page_{nullptr}; - DisplayPage *previous_page_{nullptr}; - std::vector on_page_change_triggers_; - bool auto_clear_enabled_{true}; - std::vector clipping_rectangle_; -}; - -class DisplayPage { - public: - DisplayPage(display_writer_t writer); - void show(); - void show_next(); - void show_prev(); - void set_parent(DisplayBuffer *parent); - void set_prev(DisplayPage *prev); - void set_next(DisplayPage *next); - const display_writer_t &get_writer() const; - - protected: - DisplayBuffer *parent_; - display_writer_t writer_; - DisplayPage *prev_{nullptr}; - DisplayPage *next_{nullptr}; -}; - -template class DisplayPageShowAction : public Action { - public: - TEMPLATABLE_VALUE(DisplayPage *, page) - - void play(Ts... x) override { - auto *page = this->page_.value(x...); - if (page != nullptr) { - page->show(); - } - } -}; - -template class DisplayPageShowNextAction : public Action { - public: - DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {} - - void play(Ts... x) override { this->buffer_->show_next_page(); } - - DisplayBuffer *buffer_; -}; - -template class DisplayPageShowPrevAction : public Action { - public: - DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {} - - void play(Ts... x) override { this->buffer_->show_prev_page(); } - - DisplayBuffer *buffer_; -}; - -template class DisplayIsDisplayingPageCondition : public Condition { - public: - DisplayIsDisplayingPageCondition(DisplayBuffer *parent) : parent_(parent) {} - - void set_page(DisplayPage *page) { this->page_ = page; } - bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } - - protected: - DisplayBuffer *parent_; - DisplayPage *page_; -}; - -class DisplayOnPageChangeTrigger : public Trigger { - public: - explicit DisplayOnPageChangeTrigger(DisplayBuffer *parent) { parent->add_on_page_change_trigger(this); } - void process(DisplayPage *from, DisplayPage *to); - void set_from(DisplayPage *p) { this->from_ = p; } - void set_to(DisplayPage *p) { this->to_ = p; } - - protected: - DisplayPage *from_{nullptr}; - DisplayPage *to_{nullptr}; }; } // namespace display diff --git a/esphome/components/display/rect.cpp b/esphome/components/display/rect.cpp new file mode 100644 index 0000000000..6e91c86c4f --- /dev/null +++ b/esphome/components/display/rect.cpp @@ -0,0 +1,98 @@ +#include "rect.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace display { + +static const char *const TAG = "display"; + +void Rect::expand(int16_t horizontal, int16_t vertical) { + if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) { + this->x = this->x - horizontal; + this->y = this->y - vertical; + this->w = this->w + (2 * horizontal); + this->h = this->h + (2 * vertical); + } +} + +void Rect::extend(Rect rect) { + if (!this->is_set()) { + this->x = rect.x; + this->y = rect.y; + this->w = rect.w; + this->h = rect.h; + } else { + if (this->x > rect.x) { + this->w = this->w + (this->x - rect.x); + this->x = rect.x; + } + if (this->y > rect.y) { + this->h = this->h + (this->y - rect.y); + this->y = rect.y; + } + if (this->x2() < rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->y2() < rect.y2()) { + this->h = rect.y2() - this->y; + } + } +} +void Rect::shrink(Rect rect) { + if (!this->inside(rect)) { + (*this) = Rect(); + } else { + if (this->x2() > rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->x < rect.x) { + this->w = this->w + (this->x - rect.x); + this->x = rect.x; + } + if (this->y2() > rect.y2()) { + this->h = rect.y2() - this->y; + } + if (this->y < rect.y) { + this->h = this->h + (this->y - rect.y); + this->y = rect.y; + } + } +} + +bool Rect::equal(Rect rect) { + return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h); +} + +bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT + if (!this->is_set()) { + return true; + } + if (absolute) { + return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2())); + } else { + return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h)); + } +} + +bool Rect::inside(Rect rect, bool absolute) { + if (!this->is_set() || !rect.is_set()) { + return true; + } + if (absolute) { + return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); + } else { + return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); + } +} + +void Rect::info(const std::string &prefix) { + if (this->is_set()) { + ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(), + this->y2()); + } else + ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); +} + +} // namespace display +} // namespace esphome diff --git a/esphome/components/display/rect.h b/esphome/components/display/rect.h new file mode 100644 index 0000000000..867a9c67c7 --- /dev/null +++ b/esphome/components/display/rect.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace display { + +static const int16_t VALUE_NO_SET = 32766; + +class Rect { + public: + int16_t x; ///< X coordinate of corner + int16_t y; ///< Y coordinate of corner + int16_t w; ///< Width of region + int16_t h; ///< Height of region + + Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT + inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {} + inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner + inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner + + inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); } + + void expand(int16_t horizontal, int16_t vertical); + + void extend(Rect rect); + void shrink(Rect rect); + + bool inside(Rect rect, bool absolute = true); + bool inside(int16_t test_x, int16_t test_y, bool absolute = true); + bool equal(Rect rect); + void info(const std::string &prefix = "rect info:"); +}; + +} // namespace display +} // namespace esphome diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index f4f8305ba6..9f56dc3465 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -19,6 +19,7 @@ CONF_CRC_CHECK = "crc_check" CONF_DECRYPTION_KEY = "decryption_key" CONF_DSMR_ID = "dsmr_id" CONF_GAS_MBUS_ID = "gas_mbus_id" +CONF_WATER_MBUS_ID = "water_mbus_id" CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length" CONF_REQUEST_INTERVAL = "request_interval" CONF_REQUEST_PIN = "request_pin" @@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DECRYPTION_KEY): _validate_key, cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, + cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, cv.Optional( @@ -82,9 +84,10 @@ async def to_code(config): cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) + cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) # DSMR Parser - cg.add_library("glmnet/Dsmr", "0.5") + cg.add_library("glmnet/Dsmr", "0.8") # Crypto cg.add_library("rweather/Crypto", "0.4.0") diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 0b0439baa4..f2398d1908 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_WATER, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, @@ -236,6 +237,36 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional("water_delivered"): sensor.sensor_schema( + unit_of_measurement=UNIT_CUBIC_METER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_WATER, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional( + "active_energy_import_current_average_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "active_energy_import_maximum_demand_running_month" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "active_energy_import_maximum_demand_last_13_months" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/duty_time/__init__.py b/esphome/components/duty_time/__init__.py new file mode 100644 index 0000000000..b708cee80b --- /dev/null +++ b/esphome/components/duty_time/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@dudanov"] diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp new file mode 100644 index 0000000000..045cbcceac --- /dev/null +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -0,0 +1,103 @@ +#include "duty_time_sensor.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace duty_time_sensor { + +static const char *const TAG = "duty_time_sensor"; + +void DutyTimeSensor::set_sensor(binary_sensor::BinarySensor *const sensor) { + sensor->add_on_state_callback([this](bool state) { this->process_state_(state); }); +} + +void DutyTimeSensor::start() { + if (!this->last_state_) + this->process_state_(true); +} + +void DutyTimeSensor::stop() { + if (this->last_state_) + this->process_state_(false); +} + +void DutyTimeSensor::update() { + if (this->last_state_) + this->process_state_(true); +} + +void DutyTimeSensor::loop() { + if (this->func_ == nullptr) + return; + + const bool state = this->func_(); + + if (state != this->last_state_) + this->process_state_(state); +} + +void DutyTimeSensor::setup() { + uint32_t seconds = 0; + + if (this->restore_) { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_.load(&seconds); + } + + this->set_value_(seconds); +} + +void DutyTimeSensor::set_value_(const uint32_t sec) { + this->last_time_ = 0; + if (this->last_state_) + this->last_time_ = millis(); // last time with 0 ms correction + this->publish_and_save_(sec, 0); +} + +void DutyTimeSensor::process_state_(const bool state) { + const uint32_t now = millis(); + + if (this->last_state_) { + // update or falling edge + const uint32_t tm = now - this->last_time_; + const uint32_t ms = tm % 1000; + + this->publish_and_save_(this->total_sec_ + tm / 1000, ms); + this->last_time_ = now - ms; // store time with ms correction + + if (!state) { + // falling edge + this->last_time_ = ms; // temporary store ms correction only + this->last_state_ = false; + + if (this->last_duty_time_sensor_ != nullptr) { + const uint32_t turn_on_ms = now - this->edge_time_; + this->last_duty_time_sensor_->publish_state(turn_on_ms * 1e-3f); + } + } + + } else if (state) { + // rising edge + this->last_time_ = now - this->last_time_; // store time with ms correction + this->edge_time_ = now; // store turn-on start time + this->last_state_ = true; + } +} + +void DutyTimeSensor::publish_and_save_(const uint32_t sec, const uint32_t ms) { + this->total_sec_ = sec; + this->publish_state(sec + ms * 1e-3f); + + if (this->restore_) + this->pref_.save(&sec); +} + +void DutyTimeSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Duty Time:"); + ESP_LOGCONFIG(TAG, " Update Interval: %dms", this->get_update_interval()); + ESP_LOGCONFIG(TAG, " Restore: %s", ONOFF(this->restore_)); + LOG_SENSOR(" ", "Duty Time Sensor:", this); + LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_); +} + +} // namespace duty_time_sensor +} // namespace esphome diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h new file mode 100644 index 0000000000..27fa383847 --- /dev/null +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -0,0 +1,88 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace duty_time_sensor { + +class DutyTimeSensor : public sensor::Sensor, public PollingComponent { + public: + void setup() override; + void update() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void start(); + void stop(); + bool is_running() const { return this->last_state_; } + void reset() { this->set_value_(0); } + + void set_lambda(std::function &&func) { this->func_ = func; } + void set_sensor(binary_sensor::BinarySensor *sensor); + void set_last_duty_time_sensor(sensor::Sensor *sensor) { this->last_duty_time_sensor_ = sensor; } + void set_restore(bool restore) { this->restore_ = restore; } + + protected: + void set_value_(uint32_t sec); + void process_state_(bool state); + void publish_and_save_(uint32_t sec, uint32_t ms); + + std::function func_{nullptr}; + sensor::Sensor *last_duty_time_sensor_{nullptr}; + ESPPreferenceObject pref_; + + uint32_t total_sec_; + uint32_t last_time_; + uint32_t edge_time_; + bool last_state_{false}; + bool restore_; +}; + +template class StartAction : public Action { + public: + explicit StartAction(DutyTimeSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->start(); } + + protected: + DutyTimeSensor *parent_; +}; + +template class StopAction : public Action { + public: + explicit StopAction(DutyTimeSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->stop(); } + + protected: + DutyTimeSensor *parent_; +}; + +template class ResetAction : public Action { + public: + explicit ResetAction(DutyTimeSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->reset(); } + + protected: + DutyTimeSensor *parent_; +}; + +template class RunningCondition : public Condition { + public: + explicit RunningCondition(DutyTimeSensor *parent, bool state) : parent_(parent), state_(state) {} + + bool check(Ts... x) override { return this->parent_->is_running() == this->state_; } + + protected: + DutyTimeSensor *parent_; + bool state_; +}; + +} // namespace duty_time_sensor +} // namespace esphome diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py new file mode 100644 index 0000000000..5f8582d481 --- /dev/null +++ b/esphome/components/duty_time/sensor.py @@ -0,0 +1,121 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.automation import ( + Action, + Condition, + maybe_simple_id, + register_action, + register_condition, +) +from esphome.components import binary_sensor, sensor +from esphome.const import ( + CONF_ID, + CONF_SENSOR, + CONF_RESTORE, + CONF_LAMBDA, + UNIT_SECOND, + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, + DEVICE_CLASS_DURATION, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +CONF_LAST_TIME = "last_time" + +duty_time_sensor_ns = cg.esphome_ns.namespace("duty_time_sensor") +DutyTimeSensor = duty_time_sensor_ns.class_( + "DutyTimeSensor", sensor.Sensor, cg.PollingComponent +) +StartAction = duty_time_sensor_ns.class_("StartAction", Action) +StopAction = duty_time_sensor_ns.class_("StopAction", Action) +ResetAction = duty_time_sensor_ns.class_("ResetAction", Action) +SetAction = duty_time_sensor_ns.class_("SetAction", Action) +RunningCondition = duty_time_sensor_ns.class_("RunningCondition", Condition) + + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + DutyTimeSensor, + unit_of_measurement=UNIT_SECOND, + icon="mdi:timer-play-outline", + accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=DEVICE_CLASS_DURATION, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + .extend( + { + cv.Optional(CONF_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_RESTORE, default=False): cv.boolean, + cv.Optional(CONF_LAST_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + icon="mdi:timer-marker-outline", + accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL, + device_class=DEVICE_CLASS_DURATION, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")), + cv.has_at_most_one_key(CONF_SENSOR, CONF_LAMBDA), +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + cg.add(var.set_restore(config[CONF_RESTORE])) + if CONF_SENSOR in config: + sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.bool_) + cg.add(var.set_lambda(lambda_)) + if CONF_LAST_TIME in config: + sens = await sensor.new_sensor(config[CONF_LAST_TIME]) + cg.add(var.set_last_duty_time_sensor(sens)) + + +# AUTOMATIONS + +DUTY_TIME_ID_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(DutyTimeSensor), + } +) + + +@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA) +async def sensor_runtime_start_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA) +async def sensor_runtime_stop_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA) +async def sensor_runtime_reset_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@register_condition( + "sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA +) +async def duty_time_is_running_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, True) + + +@register_condition( + "sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA +) +async def duty_time_is_not_running_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, False) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 3ca140f0d4..903031c77a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -547,6 +547,8 @@ def copy_files(): CORE.relative_build_path(f"components/{name}"), dirs_exist_ok=True, ignore=shutil.ignore_patterns(".git", ".github"), + symlinks=True, + ignore_dangling_symlinks=True, ) dir = os.path.dirname(__file__) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index bedc0a4c30..6f0f3741dd 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -35,6 +35,7 @@ ETHERNET_TYPES = { "IP101": EthernetType.ETHERNET_TYPE_IP101, "JL1101": EthernetType.ETHERNET_TYPE_JL1101, "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, + "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, } emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index 6011795033..de2a6f4f35 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -19,7 +19,11 @@ #include #include "esp_log.h" #include "esp_eth.h" +#if ESP_IDF_VERSION_MAJOR >= 5 +#include "esp_eth_phy_802_3.h" +#else #include "eth_phy_regs_struct.h" +#endif #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" @@ -170,7 +174,11 @@ static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) { return ESP_OK; } +#if ESP_IDF_VERSION_MAJOR >= 5 +static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy, eth_phy_autoneg_cmd_t cmd, bool *nego_state) { +#else static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) { +#endif phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); esp_eth_mediator_t *eth = jl1101->eth; /* in case any link status has changed, let's assume we're in link down status */ @@ -285,7 +293,11 @@ static esp_err_t jl1101_init(esp_eth_phy_t *phy) { esp_eth_mediator_t *eth = jl1101->eth; // Detect PHY address if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) { +#if ESP_IDF_VERSION_MAJOR >= 5 + PHY_CHECK(esp_eth_phy_802_3_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); +#else PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); +#endif } /* Power on Ethernet PHY */ PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err); @@ -324,7 +336,11 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { jl1101->parent.init = jl1101_init; jl1101->parent.deinit = jl1101_deinit; jl1101->parent.set_mediator = jl1101_set_mediator; +#if ESP_IDF_VERSION_MAJOR >= 5 + jl1101->parent.autonego_ctrl = jl1101_negotiate; +#else jl1101->parent.negotiate = jl1101_negotiate; +#endif jl1101->parent.get_link = jl1101_get_link; jl1101->parent.pwrctl = jl1101_pwrctl; jl1101->parent.get_addr = jl1101_get_addr; diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 0487ea5498..3b5804abdd 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -41,18 +41,27 @@ void EthernetComponent::setup() { this->eth_netif_ = esp_netif_new(&cfg); // Init MAC and PHY configs to default - eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); - phy_config.phy_addr = this->phy_addr_; phy_config.reset_gpio_num = this->power_pin_; + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); +#if ESP_IDF_VERSION_MAJOR >= 5 + eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); + esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; + esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; + esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; + esp32_emac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; + + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); +#else mac_config.smi_mdc_gpio_num = this->mdc_pin_; mac_config.smi_mdio_gpio_num = this->mdio_pin_; mac_config.clock_config.rmii.clock_mode = this->clk_mode_; mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config); +#endif switch (this->type_) { case ETHERNET_TYPE_LAN8720: { @@ -75,8 +84,13 @@ void EthernetComponent::setup() { this->phy_ = esp_eth_phy_new_jl1101(&phy_config); break; } - case ETHERNET_TYPE_KSZ8081: { + case ETHERNET_TYPE_KSZ8081: + case ETHERNET_TYPE_KSZ8081RNA: { +#if ESP_IDF_VERSION_MAJOR >= 5 + this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); +#else this->phy_ = esp_eth_phy_new_ksz8081(&phy_config); +#endif break; } default: { @@ -89,6 +103,12 @@ void EthernetComponent::setup() { this->eth_handle_ = nullptr; err = esp_eth_driver_install(ð_config, &this->eth_handle_); ESPHL_ERROR_CHECK(err, "ETH driver install error"); + + if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { + // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. + this->ksz8081_set_clock_reference_(mac); + } + /* attach Ethernet driver to TCP/IP stack */ err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); ESPHL_ERROR_CHECK(err, "ETH netif attach error"); @@ -171,6 +191,10 @@ void EthernetComponent::dump_config() { eth_type = "KSZ8081"; break; + case ETHERNET_TYPE_KSZ8081RNA: + eth_type = "KSZ8081RNA"; + break; + default: eth_type = "Unknown"; break; @@ -221,13 +245,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base return; } - ESP_LOGV(TAG, "[Ethernet event] %s (num=%d)", event_name, event); + ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event); } void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { global_eth_component->connected_ = true; - ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%d)", event_id); + ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%" PRId32 ")", event_id); } void EthernetComponent::start_connect_() { @@ -372,6 +396,37 @@ bool EthernetComponent::powerdown() { return true; } +void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { +#define KSZ80XX_PC2R_REG_ADDR (0x1F) + + esp_err_t err; + + uint32_t phy_control_2; + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); + + /* + * Bit 7 is `RMII Reference Clock Select`. Default is `0`. + * KSZ8081RNA: + * 0 - clock input to XI (Pin 8) is 25 MHz for RMII – 25 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode. + * KSZ8081RND: + * 0 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII – 25 MHz clock mode. + */ + if ((phy_control_2 & (1 << 7)) != (1 << 7)) { + phy_control_2 |= 1 << 7; + err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2); + ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); + } + +#undef KSZ80XX_PC2R_REG_ADDR +} + } // namespace ethernet } // namespace esphome diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 918e47212f..f6b67f3f82 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -21,6 +21,7 @@ enum EthernetType { ETHERNET_TYPE_IP101, ETHERNET_TYPE_JL1101, ETHERNET_TYPE_KSZ8081, + ETHERNET_TYPE_KSZ8081RNA, }; struct ManualIP { @@ -67,6 +68,8 @@ class EthernetComponent : public Component { void start_connect_(); void dump_connect_params_(); + /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. + void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); std::string use_address_; uint8_t phy_addr_{0}; diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index aa165ebaa5..52f877d986 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -3,11 +3,11 @@ from pathlib import Path import hashlib import os import re +from packaging import version import requests from esphome import core -from esphome.components import display import esphome.config_validation as cv import esphome.codegen as cg from esphome.helpers import copy_file_if_changed @@ -29,9 +29,11 @@ DOMAIN = "font" DEPENDENCIES = ["display"] MULTI_CONF = True -Font = display.display_ns.class_("Font") -Glyph = display.display_ns.class_("Glyph") -GlyphData = display.display_ns.struct("GlyphData") +font_ns = cg.esphome_ns.namespace("font") + +Font = font_ns.class_("Font") +Glyph = font_ns.class_("Glyph") +GlyphData = font_ns.struct("GlyphData") def validate_glyphs(value): @@ -65,13 +67,18 @@ def validate_pillow_installed(value): except ImportError as err: raise cv.Invalid( "Please install the pillow python package to use this feature. " - "(pip install pillow)" + '(pip install pillow">4.0.0,<10.0.0")' ) from err - if PIL.__version__[0] < "4": + if version.parse(PIL.__version__) < version.parse("4.0.0"): raise cv.Invalid( "Please update your pillow installation to at least 4.0.x. " - "(pip install -U pillow)" + '(pip install pillow">4.0.0,<10.0.0")' + ) + if version.parse(PIL.__version__) >= version.parse("10.0.0"): + raise cv.Invalid( + "Please downgrade your pillow installation to below 10.0.0. " + '(pip install pillow">4.0.0,<10.0.0")' ) return value diff --git a/esphome/components/display/font.cpp b/esphome/components/font/font.cpp similarity index 56% rename from esphome/components/display/font.cpp rename to esphome/components/font/font.cpp index 1833ef5023..ef5b2b788d 100644 --- a/esphome/components/display/font.cpp +++ b/esphome/components/font/font.cpp @@ -1,18 +1,35 @@ #include "font.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/color.h" +#include "esphome/components/display/display_buffer.h" namespace esphome { -namespace display { +namespace font { -bool Glyph::get_pixel(int x, int y) const { - const int x_data = x - this->glyph_data_->offset_x; - const int y_data = y - this->glyph_data_->offset_y; - if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height) - return false; - const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u; - const uint32_t pos = x_data + y_data * width_8; - return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u)); +static const char *const TAG = "font"; + +void Glyph::draw(int x_at, int y_start, display::Display *display, Color color) const { + int scan_x1, scan_y1, scan_width, scan_height; + this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); + + const unsigned char *data = this->glyph_data_->data; + const int max_x = x_at + scan_x1 + scan_width; + const int max_y = y_start + scan_y1 + scan_height; + + for (int glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) { + for (int glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) { + uint8_t pixel_data = progmem_read_byte(data); + const int pixel_max_x = std::min(max_x, glyph_x + 8); + + for (int pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) { + if (pixel_data & 0x80) { + display->draw_pixel_at(pixel_x, glyph_y, color); + } + } + } + } } const char *Glyph::get_char() const { return this->glyph_data_->a_char; } bool Glyph::compare_to(const char *str) const { @@ -47,6 +64,12 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { *width = this->glyph_data_->width; *height = this->glyph_data_->height; } + +Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { + glyphs_.reserve(data_nr); + for (int i = 0; i < data_nr; ++i) + glyphs_.emplace_back(&data[i]); +} int Font::match_next_glyph(const char *str, int *match_length) { int lo = 0; int hi = this->glyphs_.size() - 1; @@ -95,11 +118,32 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in *x_offset = min_x; *width = x - min_x; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { - glyphs_.reserve(data_nr); - for (int i = 0; i < data_nr; ++i) - glyphs_.emplace_back(&data[i]); +void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text) { + int i = 0; + int x_at = x_start; + while (text[i] != '\0') { + int match_length; + int glyph_n = this->match_next_glyph(text + i, &match_length); + if (glyph_n < 0) { + // Unknown char, skip + ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); + if (!this->get_glyphs().empty()) { + uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width; + display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); + x_at += glyph_width; + } + + i++; + continue; + } + + const Glyph &glyph = this->get_glyphs()[glyph_n]; + glyph.draw(x_at, y_start, display, color); + x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; + + i += match_length; + } } -} // namespace display +} // namespace font } // namespace esphome diff --git a/esphome/components/display/font.h b/esphome/components/font/font.h similarity index 78% rename from esphome/components/display/font.h rename to esphome/components/font/font.h index 08b457116e..03171a6126 100644 --- a/esphome/components/display/font.h +++ b/esphome/components/font/font.h @@ -1,11 +1,12 @@ #pragma once #include "esphome/core/datatypes.h" +#include "esphome/core/color.h" +#include "esphome/components/display/display_buffer.h" namespace esphome { -namespace display { +namespace font { -class DisplayBuffer; class Font; struct GlyphData { @@ -21,7 +22,7 @@ class Glyph { public: Glyph(const GlyphData *data) : glyph_data_(data) {} - bool get_pixel(int x, int y) const; + void draw(int x, int y, display::Display *display, Color color) const; const char *get_char() const; @@ -33,12 +34,11 @@ class Glyph { protected: friend Font; - friend DisplayBuffer; const GlyphData *glyph_data_; }; -class Font { +class Font : public display::BaseFont { public: /** Construct the font with the given glyphs. * @@ -50,7 +50,8 @@ class Font { int match_next_glyph(const char *str, int *match_length); - void measure(const char *str, int *width, int *x_offset, int *baseline, int *height); + void print(int x_start, int y_start, display::Display *display, Color color, const char *text) override; + void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override; inline int get_baseline() { return this->baseline_; } inline int get_height() { return this->height_; } @@ -62,5 +63,5 @@ class Font { int height_; }; -} // namespace display +} // namespace font } // namespace esphome diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index c229f17dd8..294e16dbb1 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -1,5 +1,5 @@ #include "graph.h" -#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display.h" #include "esphome/core/color.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -56,7 +56,7 @@ void GraphTrace::init(Graph *g) { this->data_.set_update_time_ms(g->get_duration() * 1000 / g->get_width()); } -void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) { +void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) { /// Plot border if (this->border_) { buff->horizontal_line(x_offset, y_offset, this->width_, color); @@ -303,7 +303,7 @@ void GraphLegend::init(Graph *g) { } } -void Graph::draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) { +void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) { if (!legend_) return; diff --git a/esphome/components/graph/graph.h b/esphome/components/graph/graph.h index 69c1167f54..339a6f6d94 100644 --- a/esphome/components/graph/graph.h +++ b/esphome/components/graph/graph.h @@ -8,10 +8,10 @@ namespace esphome { -// forward declare DisplayBuffer +// forward declare Display namespace display { -class DisplayBuffer; -class Font; +class Display; +class BaseFont; } // namespace display namespace graph { @@ -45,8 +45,8 @@ enum ValuePositionType { class GraphLegend { public: void init(Graph *g); - void set_name_font(display::Font *font) { this->font_label_ = font; } - void set_value_font(display::Font *font) { this->font_value_ = font; } + void set_name_font(display::BaseFont *font) { this->font_label_ = font; } + void set_value_font(display::BaseFont *font) { this->font_value_ = font; } void set_width(uint32_t width) { this->width_ = width; } void set_height(uint32_t height) { this->height_ = height; } void set_border(bool val) { this->border_ = val; } @@ -63,8 +63,8 @@ class GraphLegend { ValuePositionType values_{VALUE_POSITION_TYPE_AUTO}; bool units_{true}; DirectionType direction_{DIRECTION_TYPE_AUTO}; - display::Font *font_label_{nullptr}; - display::Font *font_value_{nullptr}; + display::BaseFont *font_label_{nullptr}; + display::BaseFont *font_value_{nullptr}; // Calculated values Graph *parent_{nullptr}; // (x0) (xs,ys) (xs,ys) @@ -133,8 +133,8 @@ class GraphTrace { class Graph : public Component { public: - void draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color); - void draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color); + void draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color); + void draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color); void setup() override; float get_setup_priority() const override { return setup_priority::PROCESSOR; } diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py new file mode 100644 index 0000000000..75610ce9d3 --- /dev/null +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -0,0 +1,152 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import i2c + +from esphome.const import ( + CONF_ID, + CONF_CHANNEL, + CONF_SPEED, + CONF_DIRECTION, +) + +DEPENDENCIES = ["i2c"] + +CODEOWNERS = ["@max246"] + +grove_tb6612fng_ns = cg.esphome_ns.namespace("grove_tb6612fng") +GROVE_TB6612FNG = grove_tb6612fng_ns.class_( + "GroveMotorDriveTB6612FNG", cg.Component, i2c.I2CDevice +) +GROVETB6612FNGMotorRunAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorRunAction", automation.Action +) +GROVETB6612FNGMotorBrakeAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorBrakeAction", automation.Action +) +GROVETB6612FNGMotorStopAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorStopAction", automation.Action +) +GROVETB6612FNGMotorStandbyAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorStandbyAction", automation.Action +) +GROVETB6612FNGMotorNoStandbyAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorNoStandbyAction", automation.Action +) + +DIRECTION_TYPE = { + "FORWARD": 1, + "BACKWARD": 2, +} + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(GROVE_TB6612FNG), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x14)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + +@automation.register_action( + "grove_tb6612fng.run", + GROVETB6612FNGMotorRunAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + cv.Required(CONF_SPEED): cv.templatable(cv.int_range(min=0, max=255)), + cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True), + } + ), +) +async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16) + template_speed = ( + template_speed if config[CONF_DIRECTION] == "FORWARD" else -template_speed + ) + cg.add(var.set_channel(template_channel)) + cg.add(var.set_speed(template_speed)) + return var + + +@automation.register_action( + "grove_tb6612fng.break", + GROVETB6612FNGMotorBrakeAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + } + ), +) +async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + cg.add(var.set_channel(template_channel)) + return var + + +@automation.register_action( + "grove_tb6612fng.stop", + GROVETB6612FNGMotorStopAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + } + ), +) +async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + cg.add(var.set_channel(template_channel)) + return var + + +@automation.register_action( + "grove_tb6612fng.standby", + GROVETB6612FNGMotorStandbyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + } + ), +) +async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + return var + + +@automation.register_action( + "grove_tb6612fng.no_standby", + GROVETB6612FNGMotorNoStandbyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + } + ), +) +async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + return var diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp new file mode 100644 index 0000000000..621b7968a4 --- /dev/null +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -0,0 +1,171 @@ +#include "grove_tb6612fng.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace grove_tb6612fng { + +static const char *const TAG = "GroveMotorDriveTB6612FNG"; + +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE = 0x00; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STOP = 0x01; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CW = 0x02; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CCW = 0x03; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY = 0x04; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY = 0x05; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN = 0x06; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP = 0x07; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN = 0x08; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR = 0x11; + +void GroveMotorDriveTB6612FNG::dump_config() { + ESP_LOGCONFIG(TAG, "GroveMotorDriveTB6612FNG:"); + LOG_I2C_DEVICE(this); +} + +void GroveMotorDriveTB6612FNG::setup() { + ESP_LOGCONFIG(TAG, "Setting up Grove Motor Drive TB6612FNG ..."); + if (!this->standby()) { + this->mark_failed(); + return; + } +} + +bool GroveMotorDriveTB6612FNG::standby() { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY, &status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set standby failed!"); + this->status_set_warning(); + return false; + } + return true; +} + +bool GroveMotorDriveTB6612FNG::not_standby() { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY, &status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set not standby failed!"); + this->status_set_warning(); + return false; + } + return true; +} + +void GroveMotorDriveTB6612FNG::set_i2c_addr(uint8_t addr) { + if (addr == 0x00 || addr >= 0x80) { + return; + } + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR, &addr, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set new i2c address failed!"); + this->status_set_warning(); + return; + } + this->set_i2c_address(addr); +} + +void GroveMotorDriveTB6612FNG::dc_motor_run(uint8_t channel, int16_t speed) { + speed = clamp(speed, -255, 255); + + buffer_[0] = channel; + if (speed >= 0) { + buffer_[1] = speed; + } else { + buffer_[1] = (uint8_t) (-speed); + } + + if (speed >= 0) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, buffer_, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run motor failed!"); + this->status_set_warning(); + return; + } + } else { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, buffer_, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run motor failed!"); + this->status_set_warning(); + return; + } + } +} + +void GroveMotorDriveTB6612FNG::dc_motor_brake(uint8_t channel) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE, &channel, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Break motor failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::dc_motor_stop(uint8_t channel) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STOP, &channel, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Stop dc motor failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm) { + uint8_t cw = 0; + // 0.1ms_per_step + uint16_t ms_per_step = 0; + + if (steps > 0) { + cw = 1; + } + // stop + else if (steps == 0) { + this->stepper_stop(); + return; + } else if (steps == INT16_MIN) { + steps = INT16_MAX; + } else { + steps = -steps; + } + + rpm = clamp(rpm, 1, 300); + + ms_per_step = (uint16_t) (3000.0 / (float) rpm); + buffer_[0] = mode; + buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw + buffer_[2] = steps; + buffer_[3] = (steps >> 8); + buffer_[4] = ms_per_step; + buffer_[5] = (ms_per_step >> 8); + + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run stepper failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_stop() { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Send stop stepper failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw) { + // 4=>infinite ccw 5=>infinite cw + uint8_t cw = (is_cw) ? 5 : 4; + // 0.1ms_per_step + uint16_t ms_per_step = 0; + + rpm = clamp(rpm, 1, 300); + ms_per_step = (uint16_t) (3000.0 / (float) rpm); + + buffer_[0] = mode; + buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw + buffer_[2] = ms_per_step; + buffer_[3] = (ms_per_step >> 8); + + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN, buffer_, 4) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Write stepper keep run failed"); + this->status_set_warning(); + return; + } +} +} // namespace grove_tb6612fng +} // namespace esphome diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h new file mode 100644 index 0000000000..ccdab6472a --- /dev/null +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -0,0 +1,208 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/automation.h" +//#include "esphome/core/helpers.h" + +/* + Grove_Motor_Driver_TB6612FNG.h + A library for the Grove - Motor Driver(TB6612FNG) + Copyright (c) 2018 seeed technology co., ltd. + Website : www.seeed.cc + Author : Jerry Yip + Create Time: 2018-06 + Version : 0.1 + Change Log : + The MIT License (MIT) + 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. +*/ + +namespace esphome { +namespace grove_tb6612fng { + +enum MotorChannelTypeT { + MOTOR_CHA = 0, + MOTOR_CHB = 1, +}; + +enum StepperModeTypeT { + FULL_STEP = 0, + WAVE_DRIVE = 1, + HALF_STEP = 2, + MICRO_STEPPING = 3, +}; + +class GroveMotorDriveTB6612FNG : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + /************************************************************* + Description + Enter standby mode. Normally you don't need to call this, except that + you have called notStandby() before. + Parameter + Null. + Return + True/False. + *************************************************************/ + bool standby(); + + /************************************************************* + Description + Exit standby mode. Motor driver does't do any action at this mode. + Parameter + Null. + Return + True/False. + *************************************************************/ + bool not_standby(); + + /************************************************************* + Description + Set an new I2C address. + Parameter + addr: 0x01~0x7f + Return + Null. + *************************************************************/ + void set_i2c_addr(uint8_t addr); + + /************************************************************* + Description + Drive a motor. + Parameter + chl: MOTOR_CHA or MOTOR_CHB + speed: -255~255, if speed > 0, motor moves clockwise. + Note that there is always a starting speed(a starting voltage) for motor. + If the input voltage is 5V, the starting speed should larger than 100 or + smaller than -100. + Return + Null. + *************************************************************/ + void dc_motor_run(uint8_t channel, int16_t speed); + + /************************************************************* + Description + Brake, stop the motor immediately + Parameter + chl: MOTOR_CHA or MOTOR_CHB + Return + Null. + *************************************************************/ + void dc_motor_brake(uint8_t channel); + + /************************************************************* + Description + Stop the motor slowly. + Parameter + chl: MOTOR_CHA or MOTOR_CHB + Return + Null. + *************************************************************/ + void dc_motor_stop(uint8_t channel); + + /************************************************************* + Description + Drive a stepper. + Parameter + mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING, + for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png + steps: The number of steps to run, range from -32768 to 32767. + When steps = 0, the stepper stops. + When steps > 0, the stepper runs clockwise. When steps < 0, the stepper runs anticlockwise. + rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300. + Note that high rpm will lead to step lose, so rpm should not be larger than 150. + Return + Null. + *************************************************************/ + void stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm); + + /************************************************************* + Description + Stop a stepper. + Parameter + Null. + Return + Null. + *************************************************************/ + void stepper_stop(); + + // keeps moving(direction same as the last move, default to clockwise) + /************************************************************* + Description + Keep a stepper running. + Parameter + mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING, + for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png + rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300. + Note that high rpm will lead to step lose, so rpm should not be larger than 150. + is_cw: Set the running direction, true for clockwise and false for anti-clockwise. + Return + Null. + *************************************************************/ + void stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw); + + private: + uint8_t buffer_[16]; +}; + +template +class GROVETB6612FNGMotorRunAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint16_t, speed) + + void play(Ts... x) override { + auto channel = this->channel_.value(x...); + auto speed = this->speed_.value(x...); + this->parent_->dc_motor_run(channel, speed); + } +}; + +template +class GROVETB6612FNGMotorBrakeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + + void play(Ts... x) override { this->parent_->dc_motor_brake(this->channel_.value(x...)); } +}; + +template +class GROVETB6612FNGMotorStopAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + + void play(Ts... x) override { this->parent_->dc_motor_stop(this->channel_.value(x...)); } +}; + +template +class GROVETB6612FNGMotorStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->standby(); } +}; + +template +class GROVETB6612FNGMotorNoStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->not_standby(); } +}; + +} // namespace grove_tb6612fng +} // namespace esphome diff --git a/esphome/components/haier/__init__.py b/esphome/components/haier/__init__.py index b9ea055a41..e69de29bb2 100644 --- a/esphome/components/haier/__init__.py +++ b/esphome/components/haier/__init__.py @@ -1 +0,0 @@ -CODEOWNERS = ["@Yarikx"] diff --git a/esphome/components/haier/automation.h b/esphome/components/haier/automation.h new file mode 100644 index 0000000000..84e4554db8 --- /dev/null +++ b/esphome/components/haier/automation.h @@ -0,0 +1,130 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "haier_base.h" +#include "hon_climate.h" + +namespace esphome { +namespace haier { + +template class DisplayOnAction : public Action { + public: + DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(true); } + + protected: + HaierClimateBase *parent_; +}; + +template class DisplayOffAction : public Action { + public: + DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(false); } + + protected: + HaierClimateBase *parent_; +}; + +template class BeeperOnAction : public Action { + public: + BeeperOnAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(true); } + + protected: + HonClimate *parent_; +}; + +template class BeeperOffAction : public Action { + public: + BeeperOffAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(false); } + + protected: + HonClimate *parent_; +}; + +template class VerticalAirflowAction : public Action { + public: + VerticalAirflowAction(HonClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(AirflowVerticalDirection, direction) + void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } + + protected: + HonClimate *parent_; +}; + +template class HorizontalAirflowAction : public Action { + public: + HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction) + void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } + + protected: + HonClimate *parent_; +}; + +template class HealthOnAction : public Action { + public: + HealthOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_health_mode(true); } + + protected: + HaierClimateBase *parent_; +}; + +template class HealthOffAction : public Action { + public: + HealthOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_health_mode(false); } + + protected: + HaierClimateBase *parent_; +}; + +template class StartSelfCleaningAction : public Action { + public: + StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->start_self_cleaning(); } + + protected: + HonClimate *parent_; +}; + +template class StartSteriCleaningAction : public Action { + public: + StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->start_steri_cleaning(); } + + protected: + HonClimate *parent_; +}; + +template class PowerOnAction : public Action { + public: + PowerOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->send_power_on_command(); } + + protected: + HaierClimateBase *parent_; +}; + +template class PowerOffAction : public Action { + public: + PowerOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->send_power_off_command(); } + + protected: + HaierClimateBase *parent_; +}; + +template class PowerToggleAction : public Action { + public: + PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->toggle_power(); } + + protected: + HaierClimateBase *parent_; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index cee83232a1..12b76084ba 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -1,43 +1,364 @@ -from esphome.components import climate +import logging import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import uart -from esphome.components.climate import ClimateSwingMode -from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES +import esphome.final_validate as fv +from esphome.components import uart, sensor, climate, logger +from esphome import automation +from esphome.const import ( + CONF_BEEPER, + CONF_ID, + CONF_LEVEL, + CONF_LOGGER, + CONF_LOGS, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_PROTOCOL, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_SWING_MODES, + CONF_VISUAL, + CONF_WIFI, + DEVICE_CLASS_TEMPERATURE, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) +from esphome.components.climate import ( + ClimateSwingMode, + ClimateMode, +) -DEPENDENCIES = ["uart"] +_LOGGER = logging.getLogger(__name__) + +PROTOCOL_MIN_TEMPERATURE = 16.0 +PROTOCOL_MAX_TEMPERATURE = 30.0 +PROTOCOL_TEMPERATURE_STEP = 1.0 + +CODEOWNERS = ["@paveldn"] +AUTO_LOAD = ["sensor"] +DEPENDENCIES = ["climate", "uart"] +CONF_WIFI_SIGNAL = "wifi_signal" +CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" +CONF_VERTICAL_AIRFLOW = "vertical_airflow" +CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" + + +PROTOCOL_HON = "HON" +PROTOCOL_SMARTAIR2 = "SMARTAIR2" +PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2] haier_ns = cg.esphome_ns.namespace("haier") -HaierClimate = haier_ns.class_( - "HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice +HaierClimateBase = haier_ns.class_( + "HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component ) +HonClimate = haier_ns.class_("HonClimate", HaierClimateBase) +Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase) -ALLOWED_CLIMATE_SWING_MODES = { - "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, - "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, - "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, + +AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection") +AIRFLOW_VERTICAL_DIRECTION_OPTIONS = { + "UP": AirflowVerticalDirection.UP, + "CENTER": AirflowVerticalDirection.CENTER, + "DOWN": AirflowVerticalDirection.DOWN, } -validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) +AirflowHorizontalDirection = haier_ns.enum("AirflowHorizontalDirection") +AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = { + "LEFT": AirflowHorizontalDirection.LEFT, + "CENTER": AirflowHorizontalDirection.CENTER, + "RIGHT": AirflowHorizontalDirection.RIGHT, +} -CONFIG_SCHEMA = cv.All( +SUPPORTED_SWING_MODES_OPTIONS = { + "OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, +} + +SUPPORTED_CLIMATE_MODES_OPTIONS = { + "OFF": ClimateMode.CLIMATE_MODE_OFF, # always available + "AUTO": ClimateMode.CLIMATE_MODE_AUTO, # always available + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + + +def validate_visual(config): + if CONF_VISUAL in config: + visual_config = config[CONF_VISUAL] + if CONF_MIN_TEMPERATURE in visual_config: + min_temp = visual_config[CONF_MIN_TEMPERATURE] + if min_temp < PROTOCOL_MIN_TEMPERATURE: + raise cv.Invalid( + f"Configured visual minimum temperature {min_temp} is lower than supported by Haier protocol is {PROTOCOL_MIN_TEMPERATURE}" + ) + else: + config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = PROTOCOL_MIN_TEMPERATURE + if CONF_MAX_TEMPERATURE in visual_config: + max_temp = visual_config[CONF_MAX_TEMPERATURE] + if max_temp > PROTOCOL_MAX_TEMPERATURE: + raise cv.Invalid( + f"Configured visual maximum temperature {max_temp} is higher than supported by Haier protocol is {PROTOCOL_MAX_TEMPERATURE}" + ) + else: + config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE + else: + config[CONF_VISUAL] = { + CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE, + CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE, + } + return config + + +BASE_CONFIG_SCHEMA = ( climate.CLIMATE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(HaierClimate), - cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list( - validate_swing_modes + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True) ), + cv.Optional( + CONF_SUPPORTED_SWING_MODES, + default=[ + "OFF", + "VERTICAL", + "HORIZONTAL", + "BOTH", + ], + ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), } ) - .extend(cv.polling_component_schema("5s")) - .extend(uart.UART_DEVICE_SCHEMA), + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) ) +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Smartair2Climate), + } + ), + PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HonClimate), + cv.Optional(CONF_WIFI_SIGNAL, default=True): cv.boolean, + cv.Optional(CONF_BEEPER, default=True): cv.boolean, + cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ), + }, + key=CONF_PROTOCOL, + default_type=PROTOCOL_SMARTAIR2, + upper=True, + ), + validate_visual, +) + + +# Actions +DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action) +DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action) +BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action) +StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action) +StartSteriCleaningAction = haier_ns.class_( + "StartSteriCleaningAction", automation.Action +) +VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action) +HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action) +HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action) +HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action) +PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action) +PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action) + +HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HaierClimateBase), + } +) + +HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HonClimate), + } +) + + +@automation.register_action( + "climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA +) +async def display_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA +) +async def beeper_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +# Start self cleaning or steri-cleaning action action +@automation.register_action( + "climate.haier.start_self_cleaning", + StartSelfCleaningAction, + HAIER_HON_BASE_ACTION_SCHEMA, +) +@automation.register_action( + "climate.haier.start_steri_cleaning", + StartSteriCleaningAction, + HAIER_HON_BASE_ACTION_SCHEMA, +) +async def start_cleaning_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +# Set vertical airflow direction action +@automation.register_action( + "climate.haier.set_vertical_airflow", + VerticalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HonClimate), + cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable( + cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True) + ), + } + ), +) +async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable( + config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection + ) + cg.add(var.set_direction(template_)) + return var + + +# Set horizontal airflow direction action +@automation.register_action( + "climate.haier.set_horizontal_airflow", + HorizontalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HonClimate), + cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable( + cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True) + ), + } + ), +) +async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable( + config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection + ) + cg.add(var.set_direction(template_)) + return var + + +@automation.register_action( + "climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA +) +async def health_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA +) +async def power_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +def _final_validate(config): + full_config = fv.full_config.get() + if CONF_LOGGER in full_config: + _level = "NONE" + logger_config = full_config[CONF_LOGGER] + if CONF_LOGS in logger_config: + if "haier.protocol" in logger_config[CONF_LOGS]: + _level = logger_config[CONF_LOGS]["haier.protocol"] + else: + _level = logger_config[CONF_LEVEL] + _LOGGER.info("Detected log level for Haier protocol: %s", _level) + if _level not in logger.LOG_LEVEL_SEVERITY: + raise cv.Invalid("Unknown log level for Haier protocol") + _severity = logger.LOG_LEVEL_SEVERITY.index(_level) + cg.add_build_flag(f"-DHAIER_LOG_LEVEL={_severity}") + else: + _LOGGER.info( + "No logger component found, logging for Haier protocol is disabled" + ) + cg.add_build_flag("-DHAIER_LOG_LEVEL=0") + if ( + (CONF_WIFI_SIGNAL in config) + and (config[CONF_WIFI_SIGNAL]) + and CONF_WIFI not in full_config + ): + raise cv.Invalid( + f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + async def to_code(config): + cg.add(haier_ns.init_haier_protocol_logging()) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - await climate.register_climate(var, config) await uart.register_uart_device(var, config) + await climate.register_climate(var, config) + + if (CONF_WIFI_SIGNAL in config) and (config[CONF_WIFI_SIGNAL]): + cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) + if CONF_BEEPER in config: + cg.add(var.set_beeper_state(config[CONF_BEEPER])) + if CONF_OUTDOOR_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) + cg.add(var.set_outdoor_temperature_sensor(sens)) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) if CONF_SUPPORTED_SWING_MODES in config: cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + # https://github.com/paveldn/HaierProtocol + cg.add_library("pavlodn/HaierProtocol", "0.9.18") diff --git a/esphome/components/haier/haier.cpp b/esphome/components/haier/haier.cpp deleted file mode 100644 index cf69d483b5..0000000000 --- a/esphome/components/haier/haier.cpp +++ /dev/null @@ -1,302 +0,0 @@ -#include -#include "haier.h" -#include "esphome/core/macros.h" - -namespace esphome { -namespace haier { - -static const char *const TAG = "haier"; - -static const uint8_t TEMPERATURE = 13; -static const uint8_t HUMIDITY = 15; - -static const uint8_t MODE = 23; - -static const uint8_t FAN_SPEED = 25; - -static const uint8_t SWING = 27; - -static const uint8_t POWER = 29; -static const uint8_t POWER_MASK = 1; - -static const uint8_t SET_TEMPERATURE = 35; -static const uint8_t DECIMAL_MASK = (1 << 5); - -static const uint8_t CRC = 36; - -static const uint8_t COMFORT_PRESET_MASK = (1 << 3); - -static const uint8_t MIN_VALID_TEMPERATURE = 16; -static const uint8_t MAX_VALID_TEMPERATURE = 50; -static const float TEMPERATURE_STEP = 0.5f; - -static const uint8_t POLL_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 1, 90}; -static const uint8_t OFF_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 3, 92}; - -void HaierClimate::dump_config() { - ESP_LOGCONFIG(TAG, "Haier:"); - ESP_LOGCONFIG(TAG, " Update interval: %u", this->get_update_interval()); - this->dump_traits_(TAG); - this->check_uart_settings(9600); -} - -void HaierClimate::loop() { - if (this->available() >= sizeof(this->data_)) { - this->read_array(this->data_, sizeof(this->data_)); - if (this->data_[0] != 255 || this->data_[1] != 255) - return; - - read_state_(this->data_, sizeof(this->data_)); - } -} - -void HaierClimate::update() { - this->write_array(POLL_REQ, sizeof(POLL_REQ)); - dump_message_("Poll sent", POLL_REQ, sizeof(POLL_REQ)); -} - -climate::ClimateTraits HaierClimate::traits() { - auto traits = climate::ClimateTraits(); - - traits.set_visual_min_temperature(MIN_VALID_TEMPERATURE); - traits.set_visual_max_temperature(MAX_VALID_TEMPERATURE); - traits.set_visual_temperature_step(TEMPERATURE_STEP); - - traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL, climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY}); - - traits.set_supported_fan_modes({ - climate::CLIMATE_FAN_AUTO, - climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH, - }); - - traits.set_supported_swing_modes(this->supported_swing_modes_); - traits.set_supports_current_temperature(true); - traits.set_supports_two_point_target_temperature(false); - - traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); - traits.add_supported_preset(climate::CLIMATE_PRESET_COMFORT); - - return traits; -} - -void HaierClimate::read_state_(const uint8_t *data, uint8_t size) { - dump_message_("Received state", data, size); - - uint8_t check = data[CRC]; - - uint8_t crc = get_checksum_(data, size); - - if (check != crc) { - ESP_LOGW(TAG, "Invalid checksum"); - return; - } - - this->current_temperature = data[TEMPERATURE]; - - this->target_temperature = data[SET_TEMPERATURE] + MIN_VALID_TEMPERATURE; - - if (data[POWER] & DECIMAL_MASK) { - this->target_temperature += 0.5f; - } - - switch (data[MODE]) { - case MODE_SMART: - this->mode = climate::CLIMATE_MODE_HEAT_COOL; - break; - case MODE_COOL: - this->mode = climate::CLIMATE_MODE_COOL; - break; - case MODE_HEAT: - this->mode = climate::CLIMATE_MODE_HEAT; - break; - case MODE_ONLY_FAN: - this->mode = climate::CLIMATE_MODE_FAN_ONLY; - break; - case MODE_DRY: - this->mode = climate::CLIMATE_MODE_DRY; - break; - default: // other modes are unsupported - this->mode = climate::CLIMATE_MODE_HEAT_COOL; - } - - switch (data[FAN_SPEED]) { - case FAN_AUTO: - this->fan_mode = climate::CLIMATE_FAN_AUTO; - break; - - case FAN_MIN: - this->fan_mode = climate::CLIMATE_FAN_LOW; - break; - - case FAN_MIDDLE: - this->fan_mode = climate::CLIMATE_FAN_MEDIUM; - break; - - case FAN_MAX: - this->fan_mode = climate::CLIMATE_FAN_HIGH; - break; - } - - switch (data[SWING]) { - case SWING_OFF: - this->swing_mode = climate::CLIMATE_SWING_OFF; - break; - - case SWING_VERTICAL: - this->swing_mode = climate::CLIMATE_SWING_VERTICAL; - break; - - case SWING_HORIZONTAL: - this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - break; - - case SWING_BOTH: - this->swing_mode = climate::CLIMATE_SWING_BOTH; - break; - } - - if (data[POWER] & COMFORT_PRESET_MASK) { - this->preset = climate::CLIMATE_PRESET_COMFORT; - } else { - this->preset = climate::CLIMATE_PRESET_NONE; - } - - if ((data[POWER] & POWER_MASK) == 0) { - this->mode = climate::CLIMATE_MODE_OFF; - } - - this->publish_state(); -} - -void HaierClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) { - switch (call.get_mode().value()) { - case climate::CLIMATE_MODE_OFF: - send_data_(OFF_REQ, sizeof(OFF_REQ)); - break; - - case climate::CLIMATE_MODE_HEAT_COOL: - case climate::CLIMATE_MODE_AUTO: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_SMART; - break; - case climate::CLIMATE_MODE_HEAT: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_HEAT; - break; - case climate::CLIMATE_MODE_COOL: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_COOL; - break; - - case climate::CLIMATE_MODE_FAN_ONLY: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_ONLY_FAN; - break; - - case climate::CLIMATE_MODE_DRY: - data_[POWER] |= POWER_MASK; - data_[MODE] = MODE_DRY; - break; - } - } - - if (call.get_preset().has_value()) { - if (call.get_preset().value() == climate::CLIMATE_PRESET_COMFORT) { - data_[POWER] |= COMFORT_PRESET_MASK; - } else { - data_[POWER] &= ~COMFORT_PRESET_MASK; - } - } - - if (call.get_target_temperature().has_value()) { - float target = call.get_target_temperature().value() - MIN_VALID_TEMPERATURE; - - data_[SET_TEMPERATURE] = (uint8_t) target; - - if ((int) target == std::lroundf(target)) { - data_[POWER] &= ~DECIMAL_MASK; - } else { - data_[POWER] |= DECIMAL_MASK; - } - } - - if (call.get_fan_mode().has_value()) { - switch (call.get_fan_mode().value()) { - case climate::CLIMATE_FAN_AUTO: - data_[FAN_SPEED] = FAN_AUTO; - break; - case climate::CLIMATE_FAN_LOW: - data_[FAN_SPEED] = FAN_MIN; - break; - case climate::CLIMATE_FAN_MEDIUM: - data_[FAN_SPEED] = FAN_MIDDLE; - break; - case climate::CLIMATE_FAN_HIGH: - data_[FAN_SPEED] = FAN_MAX; - break; - - default: // other modes are unsupported - break; - } - } - - if (call.get_swing_mode().has_value()) { - switch (call.get_swing_mode().value()) { - case climate::CLIMATE_SWING_OFF: - data_[SWING] = SWING_OFF; - break; - case climate::CLIMATE_SWING_VERTICAL: - data_[SWING] = SWING_VERTICAL; - break; - case climate::CLIMATE_SWING_HORIZONTAL: - data_[SWING] = SWING_HORIZONTAL; - break; - case climate::CLIMATE_SWING_BOTH: - data_[SWING] = SWING_BOTH; - break; - } - } - - // Parts of the message that must have specific values for "send" command. - // The meaning of those values is unknown at the moment. - data_[9] = 1; - data_[10] = 77; - data_[11] = 95; - data_[17] = 0; - - // Compute checksum - uint8_t crc = get_checksum_(data_, sizeof(data_)); - data_[CRC] = crc; - - send_data_(data_, sizeof(data_)); -} - -void HaierClimate::send_data_(const uint8_t *message, uint8_t size) { - this->write_array(message, size); - - dump_message_("Sent message", message, size); -} - -void HaierClimate::dump_message_(const char *title, const uint8_t *message, uint8_t size) { - ESP_LOGV(TAG, "%s:", title); - for (int i = 0; i < size; i++) { - ESP_LOGV(TAG, " byte %02d - %d", i, message[i]); - } -} - -uint8_t HaierClimate::get_checksum_(const uint8_t *message, size_t size) { - uint8_t position = size - 1; - uint8_t crc = 0; - - for (int i = 2; i < position; i++) - crc += message[i]; - - return crc; -} - -} // namespace haier -} // namespace esphome diff --git a/esphome/components/haier/haier.h b/esphome/components/haier/haier.h deleted file mode 100644 index 5399fd187b..0000000000 --- a/esphome/components/haier/haier.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/uart/uart.h" - -namespace esphome { -namespace haier { - -enum Mode : uint8_t { MODE_SMART = 0, MODE_COOL = 1, MODE_HEAT = 2, MODE_ONLY_FAN = 3, MODE_DRY = 4 }; -enum FanSpeed : uint8_t { FAN_MAX = 0, FAN_MIDDLE = 1, FAN_MIN = 2, FAN_AUTO = 3 }; -enum SwingMode : uint8_t { SWING_OFF = 0, SWING_VERTICAL = 1, SWING_HORIZONTAL = 2, SWING_BOTH = 3 }; - -class HaierClimate : public climate::Climate, public uart::UARTDevice, public PollingComponent { - public: - void loop() override; - void update() override; - void dump_config() override; - void control(const climate::ClimateCall &call) override; - void set_supported_swing_modes(const std::set &modes) { - this->supported_swing_modes_ = modes; - } - - protected: - climate::ClimateTraits traits() override; - void read_state_(const uint8_t *data, uint8_t size); - void send_data_(const uint8_t *message, uint8_t size); - void dump_message_(const char *title, const uint8_t *message, uint8_t size); - uint8_t get_checksum_(const uint8_t *message, size_t size); - - private: - uint8_t data_[37]; - std::set supported_swing_modes_{}; -}; - -} // namespace haier -} // namespace esphome diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp new file mode 100644 index 0000000000..d9349cb8fe --- /dev/null +++ b/esphome/components/haier/haier_base.cpp @@ -0,0 +1,311 @@ +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "haier_base.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000; +constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000; +constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; +constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; +constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400; +constexpr size_t CONTROL_TIMEOUT_MS = 7000; +constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command supplied + +#if (HAIER_LOG_LEVEL > 4) +// To reduce size of binary this function only available when log level is Verbose +const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { + static const char *phase_names[] = { + "SENDING_INIT_1", + "WAITING_ANSWER_INIT_1", + "SENDING_INIT_2", + "WAITING_ANSWER_INIT_2", + "SENDING_FIRST_STATUS_REQUEST", + "WAITING_FIRST_STATUS_ANSWER", + "SENDING_ALARM_STATUS_REQUEST", + "WAITING_ALARM_STATUS_ANSWER", + "IDLE", + "SENDING_STATUS_REQUEST", + "WAITING_STATUS_ANSWER", + "SENDING_UPDATE_SIGNAL_REQUEST", + "WAITING_UPDATE_SIGNAL_ANSWER", + "SENDING_SIGNAL_LEVEL", + "WAITING_SIGNAL_LEVEL_ANSWER", + "SENDING_CONTROL", + "WAITING_CONTROL_ANSWER", + "SENDING_POWER_ON_COMMAND", + "WAITING_POWER_ON_ANSWER", + "SENDING_POWER_OFF_COMMAND", + "WAITING_POWER_OFF_ANSWER", + "UNKNOWN" // Should be the last! + }; + int phase_index = (int) phase; + if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) + phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; + return phase_names[phase_index]; +} +#endif + +HaierClimateBase::HaierClimateBase() + : haier_protocol_(*this), + protocol_phase_(ProtocolPhases::SENDING_INIT_1), + action_request_(ActionRequest::NO_ACTION), + display_status_(true), + health_mode_(false), + force_send_control_(false), + forced_publish_(false), + forced_request_status_(false), + first_control_attempt_(false), + reset_protocol_request_(false) { + this->traits_ = climate::ClimateTraits(); + this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_AUTO}); + this->traits_.set_supported_fan_modes( + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}); + this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); + this->traits_.set_supports_current_temperature(true); +} + +HaierClimateBase::~HaierClimateBase() {} + +void HaierClimateBase::set_phase_(ProtocolPhases phase) { + if (this->protocol_phase_ != phase) { +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase)); +#else + ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase); +#endif + this->protocol_phase_ = phase; + } +} + +bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now, + std::chrono::steady_clock::time_point tpoint, size_t timeout) { + return std::chrono::duration_cast(now - tpoint).count() > timeout; +} + +bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); +} + +bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); +} + +bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS); +} + +bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); +} + +bool HaierClimateBase::is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); +} + +bool HaierClimateBase::get_display_state() const { return this->display_status_; } + +void HaierClimateBase::set_display_state(bool state) { + if (this->display_status_ != state) { + this->display_status_ = state; + this->set_force_send_control_(true); + } +} + +bool HaierClimateBase::get_health_mode() const { return this->health_mode_; } + +void HaierClimateBase::set_health_mode(bool state) { + if (this->health_mode_ != state) { + this->health_mode_ = state; + this->set_force_send_control_(true); + } +} + +void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; } + +void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; } + +void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; } +void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { + this->traits_.set_supported_swing_modes(modes); + this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); // Always available + this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); // Always available +} + +void HaierClimateBase::set_supported_modes(const std::set &modes) { + this->traits_.set_supported_modes(modes); + this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available + this->traits_.add_supported_mode(climate::CLIMATE_MODE_AUTO); // Always available +} + +haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type, + uint8_t expected_request_message_type, + uint8_t answer_message_type, + uint8_t expected_answer_message_type, + ProtocolPhases expected_phase) { + haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; + if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type)) + result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type)) + result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)) + result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + if (is_message_invalid(answer_message_type)) + result = haier_protocol::HandlerError::INVALID_ANSWER; + return result; +} + +haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) { +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_)); +#else + ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_); +#endif + if (this->protocol_phase_ > ProtocolPhases::IDLE) { + this->set_phase_(ProtocolPhases::IDLE); + } else { + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + } + return haier_protocol::HandlerError::HANDLER_OK; +} + +void HaierClimateBase::setup() { + ESP_LOGI(TAG, "Haier initialization..."); + // Set timestamp here to give AC time to boot + this->last_request_timestamp_ = std::chrono::steady_clock::now(); + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + this->set_answers_handlers(); + this->haier_protocol_.set_default_timeout_handler( + std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); +} + +void HaierClimateBase::dump_config() { + LOG_CLIMATE("", "Haier Climate", this); + ESP_LOGCONFIG(TAG, " Device communication status: %s", + (this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none"); +} + +void HaierClimateBase::loop() { + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + if ((std::chrono::duration_cast(now - this->last_valid_status_timestamp_).count() > + COMMUNICATION_TIMEOUT_MS) || + (this->reset_protocol_request_)) { + if (this->protocol_phase_ >= ProtocolPhases::IDLE) { + // No status too long, reseting protocol + if (this->reset_protocol_request_) { + this->reset_protocol_request_ = false; + ESP_LOGW(TAG, "Protocol reset requested"); + } else { + ESP_LOGW(TAG, "Communication timeout, reseting protocol"); + } + this->last_valid_status_timestamp_ = now; + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + return; + } else { + // No need to reset protocol if we didn't pass initialization phase + this->last_valid_status_timestamp_ = now; + } + }; + if ((this->protocol_phase_ == ProtocolPhases::IDLE) || + (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) { + // If control message or action is pending we should send it ASAP unless we are in initialisation + // procedure or waiting for an answer + if (this->action_request_ != ActionRequest::NO_ACTION) { + this->process_pending_action(); + } else if (this->hvac_settings_.valid || this->force_send_control_) { + ESP_LOGV(TAG, "Control packet is pending..."); + this->set_phase_(ProtocolPhases::SENDING_CONTROL); + } + } + this->process_phase(now); + this->haier_protocol_.loop(); +} + +void HaierClimateBase::process_pending_action() { + ActionRequest request = this->action_request_; + if (this->action_request_ == ActionRequest::TOGGLE_POWER) { + request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF; + } + switch (request) { + case ActionRequest::TURN_POWER_ON: + this->set_phase_(ProtocolPhases::SENDING_POWER_ON_COMMAND); + break; + case ActionRequest::TURN_POWER_OFF: + this->set_phase_(ProtocolPhases::SENDING_POWER_OFF_COMMAND); + break; + case ActionRequest::TOGGLE_POWER: + case ActionRequest::NO_ACTION: + // shouldn't get here, do nothing + break; + default: + ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_); + break; + } + this->action_request_ = ActionRequest::NO_ACTION; +} + +ClimateTraits HaierClimateBase::traits() { return traits_; } + +void HaierClimateBase::control(const ClimateCall &call) { + ESP_LOGD("Control", "Control call"); + if (this->protocol_phase_ < ProtocolPhases::IDLE) { + ESP_LOGW(TAG, "Can't send control packet, first poll answer not received"); + return; // cancel the control, we cant do it without a poll answer. + } + if (this->hvac_settings_.valid) { + ESP_LOGW(TAG, "Overriding old valid settings before they were applied!"); + } + { + if (call.get_mode().has_value()) + this->hvac_settings_.mode = call.get_mode(); + if (call.get_fan_mode().has_value()) + this->hvac_settings_.fan_mode = call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->hvac_settings_.swing_mode = call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->hvac_settings_.target_temperature = call.get_target_temperature(); + if (call.get_preset().has_value()) + this->hvac_settings_.preset = call.get_preset(); + this->hvac_settings_.valid = true; + } + this->first_control_attempt_ = true; +} + +void HaierClimateBase::HvacSettings::reset() { + this->valid = false; + this->mode.reset(); + this->fan_mode.reset(); + this->swing_mode.reset(); + this->target_temperature.reset(); + this->preset.reset(); +} + +void HaierClimateBase::set_force_send_control_(bool status) { + this->force_send_control_ = status; + if (status) { + this->first_control_attempt_ = true; + } +} + +void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) { + this->haier_protocol_.send_message(command, use_crc); + this->last_request_timestamp_ = std::chrono::steady_clock::now(); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h new file mode 100644 index 0000000000..046b59af96 --- /dev/null +++ b/esphome/components/haier/haier_base.h @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +// HaierProtocol +#include + +namespace esphome { +namespace haier { + +enum class ActionRequest : uint8_t { + NO_ACTION = 0, + TURN_POWER_ON = 1, + TURN_POWER_OFF = 2, + TOGGLE_POWER = 3, + START_SELF_CLEAN = 4, // only hOn + START_STERI_CLEAN = 5, // only hOn +}; + +class HaierClimateBase : public esphome::Component, + public esphome::climate::Climate, + public esphome::uart::UARTDevice, + public haier_protocol::ProtocolStream { + public: + HaierClimateBase(); + HaierClimateBase(const HaierClimateBase &) = delete; + HaierClimateBase &operator=(const HaierClimateBase &) = delete; + ~HaierClimateBase(); + void setup() override; + void loop() override; + void control(const esphome::climate::ClimateCall &call) override; + void dump_config() override; + float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } + void set_fahrenheit(bool fahrenheit); + void set_display_state(bool state); + bool get_display_state() const; + void set_health_mode(bool state); + bool get_health_mode() const; + void send_power_on_command(); + void send_power_off_command(); + void toggle_power(); + void reset_protocol() { this->reset_protocol_request_ = true; }; + void set_supported_modes(const std::set &modes); + void set_supported_swing_modes(const std::set &modes); + size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; + size_t read_array(uint8_t *data, size_t len) noexcept override { + return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; + }; + void write_array(const uint8_t *data, size_t len) noexcept override { + esphome::uart::UARTDevice::write_array(data, len); + }; + bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; + + protected: + enum class ProtocolPhases { + UNKNOWN = -1, + // INITIALIZATION + SENDING_INIT_1 = 0, + WAITING_ANSWER_INIT_1 = 1, + SENDING_INIT_2 = 2, + WAITING_ANSWER_INIT_2 = 3, + SENDING_FIRST_STATUS_REQUEST = 4, + WAITING_FIRST_STATUS_ANSWER = 5, + SENDING_ALARM_STATUS_REQUEST = 6, + WAITING_ALARM_STATUS_ANSWER = 7, + // FUNCTIONAL STATE + IDLE = 8, + SENDING_STATUS_REQUEST = 9, + WAITING_STATUS_ANSWER = 10, + SENDING_UPDATE_SIGNAL_REQUEST = 11, + WAITING_UPDATE_SIGNAL_ANSWER = 12, + SENDING_SIGNAL_LEVEL = 13, + WAITING_SIGNAL_LEVEL_ANSWER = 14, + SENDING_CONTROL = 15, + WAITING_CONTROL_ANSWER = 16, + SENDING_POWER_ON_COMMAND = 17, + WAITING_POWER_ON_ANSWER = 18, + SENDING_POWER_OFF_COMMAND = 19, + WAITING_POWER_OFF_ANSWER = 20, + NUM_PROTOCOL_PHASES + }; +#if (HAIER_LOG_LEVEL > 4) + const char *phase_to_string_(ProtocolPhases phase); +#endif + virtual void set_answers_handlers() = 0; + virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; + virtual haier_protocol::HaierMessage get_control_message() = 0; + virtual bool is_message_invalid(uint8_t message_type) = 0; + virtual void process_pending_action(); + esphome::climate::ClimateTraits traits() override; + // Answers handlers + haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type, + uint8_t answer_message_type, uint8_t expected_answer_message_type, + ProtocolPhases expected_phase); + // Timeout handler + haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type); + // Helper functions + void set_force_send_control_(bool status); + void send_message_(const haier_protocol::HaierMessage &command, bool use_crc); + void set_phase_(ProtocolPhases phase); + bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, + size_t timeout); + bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now); + bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now); + + struct HvacSettings { + esphome::optional mode; + esphome::optional fan_mode; + esphome::optional swing_mode; + esphome::optional target_temperature; + esphome::optional preset; + bool valid; + HvacSettings() : valid(false){}; + void reset(); + }; + haier_protocol::ProtocolHandler haier_protocol_; + ProtocolPhases protocol_phase_; + ActionRequest action_request_; + uint8_t fan_mode_speed_; + uint8_t other_modes_fan_speed_; + bool display_status_; + bool health_mode_; + bool force_send_control_; + bool forced_publish_; + bool forced_request_status_; + bool first_control_attempt_; + bool reset_protocol_request_; + esphome::climate::ClimateTraits traits_; + HvacSettings hvac_settings_; + std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages + std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout + std::chrono::steady_clock::time_point last_status_request_; // To request AC status + std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp new file mode 100644 index 0000000000..3016cda397 --- /dev/null +++ b/esphome/components/haier/hon_climate.cpp @@ -0,0 +1,857 @@ +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif +#include "hon_climate.h" +#include "hon_packet.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; +constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; + +hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { + switch (direction) { + case AirflowVerticalDirection::HEALTH_UP: + return hon_protocol::VerticalSwingMode::HEALTH_UP; + case AirflowVerticalDirection::MAX_UP: + return hon_protocol::VerticalSwingMode::MAX_UP; + case AirflowVerticalDirection::UP: + return hon_protocol::VerticalSwingMode::UP; + case AirflowVerticalDirection::DOWN: + return hon_protocol::VerticalSwingMode::DOWN; + case AirflowVerticalDirection::HEALTH_DOWN: + return hon_protocol::VerticalSwingMode::HEALTH_DOWN; + default: + return hon_protocol::VerticalSwingMode::CENTER; + } +} + +hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDirection direction) { + switch (direction) { + case AirflowHorizontalDirection::MAX_LEFT: + return hon_protocol::HorizontalSwingMode::MAX_LEFT; + case AirflowHorizontalDirection::LEFT: + return hon_protocol::HorizontalSwingMode::LEFT; + case AirflowHorizontalDirection::RIGHT: + return hon_protocol::HorizontalSwingMode::RIGHT; + case AirflowHorizontalDirection::MAX_RIGHT: + return hon_protocol::HorizontalSwingMode::MAX_RIGHT; + default: + return hon_protocol::HorizontalSwingMode::CENTER; + } +} + +HonClimate::HonClimate() + : last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]), + cleaning_status_(CleaningState::NO_CLEANING), + got_valid_outdoor_temp_(false), + hvac_hardware_info_available_(false), + hvac_functions_{false, false, false, false, false}, + use_crc_(hvac_functions_[2]), + active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + outdoor_sensor_(nullptr), + send_wifi_signal_(true) { + this->traits_.set_supported_presets({ + climate::CLIMATE_PRESET_NONE, + climate::CLIMATE_PRESET_ECO, + climate::CLIMATE_PRESET_BOOST, + climate::CLIMATE_PRESET_SLEEP, + }); + this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; + this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; +} + +HonClimate::~HonClimate() {} + +void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; } + +bool HonClimate::get_beeper_state() const { return this->beeper_status_; } + +void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; } + +AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; }; + +void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { + if (direction > AirflowVerticalDirection::DOWN) { + this->vertical_direction_ = AirflowVerticalDirection::CENTER; + } else { + this->vertical_direction_ = direction; + } + this->set_force_send_control_(true); +} + +AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } + +void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { + if (direction > AirflowHorizontalDirection::RIGHT) { + this->horizontal_direction_ = AirflowHorizontalDirection::CENTER; + } else { + this->horizontal_direction_ = direction; + } + this->set_force_send_control_(true); +} + +std::string HonClimate::get_cleaning_status_text() const { + switch (this->cleaning_status_) { + case CleaningState::SELF_CLEAN: + return "Self clean"; + case CleaningState::STERI_CLEAN: + return "56°C Steri-Clean"; + default: + return "No cleaning"; + } +} + +CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_status_; } + +void HonClimate::start_self_cleaning() { + if (this->cleaning_status_ == CleaningState::NO_CLEANING) { + ESP_LOGI(TAG, "Sending self cleaning start request"); + this->action_request_ = ActionRequest::START_SELF_CLEAN; + this->set_force_send_control_(true); + } +} + +void HonClimate::start_steri_cleaning() { + if (this->cleaning_status_ == CleaningState::NO_CLEANING) { + ESP_LOGI(TAG, "Sending steri cleaning start request"); + this->action_request_ = ActionRequest::START_STERI_CLEAN; + this->set_force_send_control_(true); + } +} + +void HonClimate::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } + +haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type, + (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_1); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { + // Wrong structure + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + } + // All OK + hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data; + char tmp[9]; + tmp[8] = 0; + strncpy(tmp, answr->protocol_version, 8); + this->hvac_protocol_version_ = std::string(tmp); + strncpy(tmp, answr->software_version, 8); + this->hvac_software_version_ = std::string(tmp); + strncpy(tmp, answr->hardware_version, 8); + this->hvac_hardware_version_ = std::string(tmp); + strncpy(tmp, answr->device_name, 8); + this->hvac_device_name_ = std::string(tmp); + this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support + this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support + this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support + this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support + this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support + this->hvac_hardware_info_available_ = true; + this->set_phase_(ProtocolPhases::SENDING_INIT_2); + return result; + } else { + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + return result; + } +} + +haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type, + (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_2); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + return result; + } else { + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + return result; + } +} + +haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type, + (uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + result = this->process_status_message_(data, data_size); + if (result != haier_protocol::HandlerError::HANDLER_OK) { + ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + } else { + if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { + memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); + } else { + ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, + sizeof(hon_protocol::HaierPacketControl)); + } + if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase_(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); + } else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) || + (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) || + (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) { + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { + this->set_phase_(ProtocolPhases::IDLE); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + } + } + return result; + } else { + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + return result; + } +} + +haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION, + message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, + ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + this->set_phase_(ProtocolPhases::SENDING_SIGNAL_LEVEL); + return result; + } else { + this->set_phase_(ProtocolPhases::IDLE); + return result; + } +} + +haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, + (uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + this->set_phase_(ProtocolPhases::IDLE); + return result; +} + +haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) { + if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { + // Unexpected answer to request + this->set_phase_(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + } + if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) { + // Don't expect this answer now + this->set_phase_(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + } + memcpy(this->active_alarms_, data + 2, 8); + this->set_phase_(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::HANDLER_OK; + } else { + this->set_phase_(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + } +} + +void HonClimate::set_answers_handlers() { + // Set handlers + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION), + std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID), + std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::CONTROL), + std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION), + std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS), + std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS), + std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); +} + +void HonClimate::dump_config() { + HaierClimateBase::dump_config(); + ESP_LOGCONFIG(TAG, " Protocol version: hOn"); + if (this->hvac_hardware_info_available_) { + ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str()); + ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""), + (this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""), + (this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : "")); + ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); + } +} + +void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_INIT_1: + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) { + this->hvac_hardware_info_available_ = false; + // Indicate device capabilities: + // bit 0 - if 1 module support interactive mode + // bit 1 - if 1 module support controller-device mode + // bit 2 - if 1 module support crc + // bit 3 - if 1 module support multiple devices + // bit 4..bit 15 - not used + uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; + static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( + (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); + this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); + this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_1); + } + break; + case ProtocolPhases::SENDING_INIT_2: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID); + this->send_message_(DEVICEID_REQUEST, this->use_crc_); + this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_2); + } + break; + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + case ProtocolPhases::SENDING_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST( + (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcomandsControl::GET_USER_DATA); + this->send_message_(STATUS_REQUEST, this->use_crc_); + this->last_status_request_ = now; + this->set_phase_((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); + } + break; +#ifdef USE_WIFI + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( + (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION); + this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); + this->last_signal_request_ = now; + this->set_phase_(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); + } + break; + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; + if (wifi::global_wifi_component->is_connected()) { + wifi_status_data[1] = 0; + int8_t rssi = wifi::global_wifi_component->wifi_rssi(); + wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f); + ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]); + } else { + ESP_LOGD(TAG, "WiFi is not connected"); + wifi_status_data[1] = 1; + wifi_status_data[3] = 0; + } + haier_protocol::HaierMessage wifi_status_request((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, + wifi_status_data, sizeof(wifi_status_data)); + this->send_message_(wifi_status_request, this->use_crc_); + this->set_phase_(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + } + break; + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + break; +#else + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + this->set_phase_(ProtocolPhases::IDLE); + break; +#endif + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST( + (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS); + this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); + this->set_phase_(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER); + } + break; + case ProtocolPhases::SENDING_CONTROL: + if (this->first_control_attempt_) { + this->control_request_timestamp_ = now; + this->first_control_attempt_ = false; + } + if (this->is_control_message_timeout_exceeded_(now)) { + ESP_LOGW(TAG, "Sending control packet timeout!"); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + this->forced_request_status_ = true; + this->forced_publish_ = true; + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { + haier_protocol::HaierMessage control_message = get_control_message(); + this->send_message_(control_message, this->use_crc_); + ESP_LOGI(TAG, "Control packet sent"); + this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER); + } + break; + case ProtocolPhases::SENDING_POWER_ON_COMMAND: + case ProtocolPhases::SENDING_POWER_OFF_COMMAND: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + uint8_t pwr_cmd_buf[2] = {0x00, 0x00}; + if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND) + pwr_cmd_buf[1] = 0x01; + haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL, + ((uint16_t) hon_protocol::SubcomandsControl::SET_SINGLE_PARAMETER) + 1, + pwr_cmd_buf, sizeof(pwr_cmd_buf)); + this->send_message_(power_cmd, this->use_crc_); + this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND + ? ProtocolPhases::WAITING_POWER_ON_ANSWER + : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + } + break; + + case ProtocolPhases::WAITING_ANSWER_INIT_1: + case ProtocolPhases::WAITING_ANSWER_INIT_2: + case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: + case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: + case ProtocolPhases::WAITING_STATUS_ANSWER: + case ProtocolPhases::WAITING_CONTROL_ANSWER: + case ProtocolPhases::WAITING_POWER_ON_ANSWER: + case ProtocolPhases::WAITING_POWER_OFF_ANSWER: + break; + case ProtocolPhases::IDLE: { + if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { + this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST); + this->forced_request_status_ = false; + } +#ifdef USE_WIFI + else if (this->send_wifi_signal_ && + (std::chrono::duration_cast(now - this->last_signal_request_).count() > + SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) + this->set_phase_(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); +#endif + } break; + default: + // Shouldn't get here +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", + phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); +#else + ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); +#endif + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + break; + } +} + +haier_protocol::HaierMessage HonClimate::get_control_message() { + uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + bool has_hvac_settings = false; + if (this->hvac_settings_.valid) { + has_hvac_settings = true; + HvacSettings climate_control; + climate_control = this->hvac_settings_; + if (climate_control.mode.has_value()) { + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + out_data->ac_power = 0; + break; + case CLIMATE_MODE_AUTO: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::AUTO; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_HEAT: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::HEAT; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_DRY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_FAN_ONLY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN; + out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode + // Disabling boost and eco mode for Fan only + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + break; + case CLIMATE_MODE_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::COOL; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Set fan speed, if we are in fan mode, reject auto in fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + } + // Set swing mode + if (climate_control.swing_mode.has_value()) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_); + out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_); + break; + case CLIMATE_SWING_VERTICAL: + out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_); + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO; + out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_); + break; + case CLIMATE_SWING_BOTH: + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO; + break; + } + } + if (climate_control.target_temperature.has_value()) { + out_data->set_point = + climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16. + } + if (out_data->ac_power == 0) { + // If AC is off - no presets alowed + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_ECO: + // Eco is not supported in Fan only mode + out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_BOOST: + out_data->quiet_mode = 0; + // Boost is not supported in Fan only mode + out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_AWAY: + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_SLEEP: + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 1; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + break; + } + } + } else { + if (out_data->vertical_swing_mode != (uint8_t) hon_protocol::VerticalSwingMode::AUTO) + out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_); + if (out_data->horizontal_swing_mode != (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) + out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_); + } + out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0; + control_out_buffer[4] = 0; // This byte should be cleared before setting values + out_data->display_status = this->display_status_ ? 1 : 0; + out_data->health_mode = this->health_mode_ ? 1 : 0; + switch (this->action_request_) { + case ActionRequest::START_SELF_CLEAN: + this->action_request_ = ActionRequest::NO_ACTION; + out_data->self_cleaning_status = 1; + out_data->steri_clean = 0; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + break; + case ActionRequest::START_STERI_CLEAN: + this->action_request_ = ActionRequest::NO_ACTION; + out_data->self_cleaning_status = 0; + out_data->steri_clean = 1; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + break; + default: + // No change + break; + } + return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcomandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); +} + +haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { + if (size < sizeof(hon_protocol::HaierStatus)) + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + hon_protocol::HaierStatus packet; + if (size < sizeof(hon_protocol::HaierStatus)) + size = sizeof(hon_protocol::HaierStatus); + memcpy(&packet, packet_buffer, size); + if (packet.sensors.error_status != 0) { + ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); + } + if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { + got_valid_outdoor_temp_ = true; + float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); + if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) + this->outdoor_sensor_->publish_state(otemp); + } + bool should_publish = false; + { + // Extra modes/presets + optional old_preset = this->preset; + if (packet.control.quiet_mode != 0) { + this->preset = CLIMATE_PRESET_ECO; + } else if (packet.control.fast_mode != 0) { + this->preset = CLIMATE_PRESET_BOOST; + } else if (packet.control.sleep_mode != 0) { + this->preset = CLIMATE_PRESET_SLEEP; + } else { + this->preset = CLIMATE_PRESET_NONE; + } + should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + } + { + // Target temperature + float old_target_temperature = this->target_temperature; + this->target_temperature = packet.control.set_point + 16.0f; + should_publish = should_publish || (old_target_temperature != this->target_temperature); + } + { + // Current temperature + float old_current_temperature = this->current_temperature; + this->current_temperature = packet.sensors.room_temperature / 2.0f; + should_publish = should_publish || (old_current_temperature != this->current_temperature); + } + { + // Fan mode + optional old_fan_mode = this->fan_mode; + // remember the fan speed we last had for climate vs fan + if (packet.control.ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN) { + if (packet.control.fan_mode != (uint8_t) hon_protocol::FanMode::FAN_AUTO) + this->fan_mode_speed_ = packet.control.fan_mode; + } else { + this->other_modes_fan_speed_ = packet.control.fan_mode; + } + switch (packet.control.fan_mode) { + case (uint8_t) hon_protocol::FanMode::FAN_AUTO: + if (packet.control.ac_mode != (uint8_t) hon_protocol::ConditioningMode::FAN) { + this->fan_mode = CLIMATE_FAN_AUTO; + } else { + // Shouldn't accept fan speed auto in fan-only mode even if AC reports it + ESP_LOGI(TAG, "Fan speed Auto is not supported in Fan only AC mode, ignoring"); + } + break; + case (uint8_t) hon_protocol::FanMode::FAN_MID: + this->fan_mode = CLIMATE_FAN_MEDIUM; + break; + case (uint8_t) hon_protocol::FanMode::FAN_LOW: + this->fan_mode = CLIMATE_FAN_LOW; + break; + case (uint8_t) hon_protocol::FanMode::FAN_HIGH: + this->fan_mode = CLIMATE_FAN_HIGH; + break; + } + should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + } + { + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status != 0; + if (disp_status != this->display_status_) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->set_force_send_control_(true); + } else { + this->display_status_ = disp_status; + } + } + } + } + { + // Health mode + bool old_health_mode = this->health_mode_; + this->health_mode_ = packet.control.health_mode == 1; + should_publish = should_publish || (old_health_mode != this->health_mode_); + } + { + CleaningState new_cleaning; + if (packet.control.steri_clean == 1) { + // Steri-cleaning + new_cleaning = CleaningState::STERI_CLEAN; + } else if (packet.control.self_cleaning_status == 1) { + // Self-cleaning + new_cleaning = CleaningState::SELF_CLEAN; + } else { + // No cleaning + new_cleaning = CleaningState::NO_CLEANING; + } + if (new_cleaning != this->cleaning_status_) { + ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); + if (new_cleaning == CleaningState::NO_CLEANING) { + // Turnuin AC off after cleaning + this->action_request_ = ActionRequest::TURN_POWER_OFF; + } + this->cleaning_status_ = new_cleaning; + } + } + { + // Climate mode + ClimateMode old_mode = this->mode; + if (packet.control.ac_power == 0) { + this->mode = CLIMATE_MODE_OFF; + } else { + // Check current hvac mode + switch (packet.control.ac_mode) { + case (uint8_t) hon_protocol::ConditioningMode::COOL: + this->mode = CLIMATE_MODE_COOL; + break; + case (uint8_t) hon_protocol::ConditioningMode::HEAT: + this->mode = CLIMATE_MODE_HEAT; + break; + case (uint8_t) hon_protocol::ConditioningMode::DRY: + this->mode = CLIMATE_MODE_DRY; + break; + case (uint8_t) hon_protocol::ConditioningMode::FAN: + this->mode = CLIMATE_MODE_FAN_ONLY; + break; + case (uint8_t) hon_protocol::ConditioningMode::AUTO: + this->mode = CLIMATE_MODE_AUTO; + break; + } + } + should_publish = should_publish || (old_mode != this->mode); + } + { + // Swing mode + ClimateSwingMode old_swing_mode = this->swing_mode; + if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) { + if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) { + this->swing_mode = CLIMATE_SWING_BOTH; + } else { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } + } else { + if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } + should_publish = should_publish || (old_swing_mode != this->swing_mode); + } + this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); + if (this->forced_publish_ || should_publish) { +#if (HAIER_LOG_LEVEL > 4) + std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); +#endif + this->publish_state(); +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGV(TAG, "Publish delay: %lld ms", + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - + _publish_start) + .count()); +#endif + this->forced_publish_ = false; + } + if (should_publish) { + ESP_LOGI(TAG, "HVAC values changed"); + } + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Set Point Status = 0x%X", packet.control.set_point); + return haier_protocol::HandlerError::HANDLER_OK; +} + +bool HonClimate::is_message_invalid(uint8_t message_type) { + return message_type == (uint8_t) hon_protocol::FrameType::INVALID; +} + +void HonClimate::process_pending_action() { + switch (this->action_request_) { + case ActionRequest::START_SELF_CLEAN: + case ActionRequest::START_STERI_CLEAN: + // Will reset action with control message sending + this->set_phase_(ProtocolPhases::SENDING_CONTROL); + break; + default: + HaierClimateBase::process_pending_action(); + break; + } +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h new file mode 100644 index 0000000000..ab913f44e2 --- /dev/null +++ b/esphome/components/haier/hon_climate.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include "esphome/components/sensor/sensor.h" +#include "haier_base.h" + +namespace esphome { +namespace haier { + +enum class AirflowVerticalDirection : uint8_t { + HEALTH_UP = 0, + MAX_UP = 1, + UP = 2, + CENTER = 3, + DOWN = 4, + HEALTH_DOWN = 5, +}; + +enum class AirflowHorizontalDirection : uint8_t { + MAX_LEFT = 0, + LEFT = 1, + CENTER = 2, + RIGHT = 3, + MAX_RIGHT = 4, +}; + +enum class CleaningState : uint8_t { + NO_CLEANING = 0, + SELF_CLEAN = 1, + STERI_CLEAN = 2, +}; + +class HonClimate : public HaierClimateBase { + public: + HonClimate(); + HonClimate(const HonClimate &) = delete; + HonClimate &operator=(const HonClimate &) = delete; + ~HonClimate(); + void dump_config() override; + void set_beeper_state(bool state); + bool get_beeper_state() const; + void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor); + AirflowVerticalDirection get_vertical_airflow() const; + void set_vertical_airflow(AirflowVerticalDirection direction); + AirflowHorizontalDirection get_horizontal_airflow() const; + void set_horizontal_airflow(AirflowHorizontalDirection direction); + std::string get_cleaning_status_text() const; + CleaningState get_cleaning_status() const; + void start_self_cleaning(); + void start_steri_cleaning(); + void set_send_wifi(bool send_wifi); + + protected: + void set_answers_handlers() override; + void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_control_message() override; + bool is_message_invalid(uint8_t message_type) override; + void process_pending_action() override; + + // Answers handlers + haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + size_t data_size); + haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + // Helper functions + haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); + std::unique_ptr last_status_message_; + bool beeper_status_; + CleaningState cleaning_status_; + bool got_valid_outdoor_temp_; + AirflowVerticalDirection vertical_direction_; + AirflowHorizontalDirection horizontal_direction_; + bool hvac_hardware_info_available_; + std::string hvac_protocol_version_; + std::string hvac_software_version_; + std::string hvac_hardware_version_; + std::string hvac_device_name_; + bool hvac_functions_[5]; + bool &use_crc_; + uint8_t active_alarms_[8]; + esphome::sensor::Sensor *outdoor_sensor_; + bool send_wifi_signal_; + std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h new file mode 100644 index 0000000000..d572ce80d9 --- /dev/null +++ b/esphome/components/haier/hon_packet.h @@ -0,0 +1,228 @@ +#pragma once + +#include + +namespace esphome { +namespace haier { +namespace hon_protocol { + +enum class VerticalSwingMode : uint8_t { + HEALTH_UP = 0x01, + MAX_UP = 0x02, + HEALTH_DOWN = 0x03, + UP = 0x04, + CENTER = 0x06, + DOWN = 0x08, + AUTO = 0x0C +}; + +enum class HorizontalSwingMode : uint8_t { + CENTER = 0x00, + MAX_LEFT = 0x03, + LEFT = 0x04, + RIGHT = 0x05, + MAX_RIGHT = 0x06, + AUTO = 0x07 +}; + +enum class ConditioningMode : uint8_t { + AUTO = 0x00, + COOL = 0x01, + DRY = 0x02, + HEALTHY_DRY = 0x03, + HEAT = 0x04, + ENERGY_SAVING = 0x05, + FAN = 0x06 +}; + +enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 }; + +enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 }; + +struct HaierPacketControl { + // Control bytes starts here + // 10 + uint8_t set_point; // Target temperature with 16°C offset (0x00 = 16°C) + // 11 + uint8_t vertical_swing_mode : 4; // See enum VerticalSwingMode + uint8_t : 0; + // 12 + uint8_t fan_mode : 3; // See enum FanMode + uint8_t special_mode : 2; // See enum SpecialMode + uint8_t ac_mode : 3; // See enum ConditioningMode + // 13 + uint8_t : 8; + // 14 + uint8_t ten_degree : 1; // 10 degree status + uint8_t display_status : 1; // If 0 disables AC's display + uint8_t half_degree : 1; // Use half degree + uint8_t intelegence_status : 1; // Intelligence status + uint8_t pmv_status : 1; // Comfort/PMV status + uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius + uint8_t : 1; + uint8_t steri_clean : 1; + // 15 + uint8_t ac_power : 1; // Is ac on or off + uint8_t health_mode : 1; // Health mode (negative ions) on or off + uint8_t electric_heating_status : 1; // Electric heating status + uint8_t fast_mode : 1; // Fast mode + uint8_t quiet_mode : 1; // Quiet mode + uint8_t sleep_mode : 1; // Sleep mode + uint8_t lock_remote : 1; // Disable remote + uint8_t beeper_status : 1; // If 1 disables AC's command feedback beeper (need to be set on every control command) + // 16 + uint8_t target_humidity; // Target humidity (0=30% .. 3C=90%, step = 1%) + // 17 + uint8_t horizontal_swing_mode : 3; // See enum HorizontalSwingMode + uint8_t : 3; + uint8_t human_sensing_status : 2; // Human sensing status + // 18 + uint8_t change_filter : 1; // Filter need replacement + uint8_t : 0; + // 19 + uint8_t fresh_air_status : 1; // Fresh air status + uint8_t humidification_status : 1; // Humidification status + uint8_t pm2p5_cleaning_status : 1; // PM2.5 cleaning status + uint8_t ch2o_cleaning_status : 1; // CH2O cleaning status + uint8_t self_cleaning_status : 1; // Self cleaning status + uint8_t light_status : 1; // Light status + uint8_t energy_saving_status : 1; // Energy saving status + uint8_t cleaning_time_status : 1; // Cleaning time (0 - accumulation, 1 - clear) +}; + +struct HaierPacketSensors { + // 20 + uint8_t room_temperature; // 0.5°C step + // 21 + uint8_t room_humidity; // 0%-100% with 1% step + // 22 + uint8_t outdoor_temperature; // 1°C step, -64°C offset (0=-64°C) + // 23 + uint8_t pm2p5_level : 2; // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad) + uint8_t air_quality : 2; // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad) + uint8_t human_sensing : 2; // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple) + uint8_t : 1; + uint8_t ac_type : 1; // 00 - Heat and cool, 01 - Cool only) + // 24 + uint8_t error_status; // See enum ErrorStatus + // 25 + uint8_t operation_source : 2; // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP) + uint8_t operation_mode_hk : 2; // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan) + uint8_t : 3; + uint8_t err_confirmation : 1; // If 1 clear error status + // 26 + uint16_t total_cleaning_time; // Cleaning cumulative time (1h step) + // 28 + uint16_t indoor_pm2p5_value; // Indoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) + // 30 + uint16_t outdoor_pm2p5_value; // Outdoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) + // 32 + uint16_t ch2o_value; // Formaldehyde value (0 ug/m3 - 10000 ug/m3, 1 ug/m3 step) + // 34 + uint16_t voc_value; // VOC value (Volatile Organic Compounds) (0 ug/m3 - 1023 ug/m3, 1 ug/m3 step) + // 36 + uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step) +}; + +struct HaierStatus { + uint16_t subcommand; + HaierPacketControl control; + HaierPacketSensors sensors; +}; + +struct DeviceVersionAnswer { + char protocol_version[8]; + char software_version[8]; + uint8_t encryption[3]; + char hardware_version[8]; + uint8_t : 8; + char device_name[8]; + uint8_t functions[2]; +}; + +// In this section comments: +// - module is the ESP32 control module (communication module in Haier protocol document) +// - device is the conditioner control board (network appliances in Haier protocol document) +enum class FrameType : uint8_t { + CONTROL = 0x01, // Requests or sets one or multiple parameters (module <-> device, required) + STATUS = 0x02, // Contains one or multiple parameters values, usually answer to control frame (module <-> device, + // required) + INVALID = 0x03, // Communication error indication (module <-> device, required) + ALARM_STATUS = 0x04, // Alarm status report (module <-> device, interactive, required) + CONFIRM = 0x05, // Acknowledgment, usually used to confirm reception of frame if there is no special answer (module + // <-> device, required) + REPORT = 0x06, // Report frame (module <-> device, interactive, required) + STOP_FAULT_ALARM = 0x09, // Stop fault alarm frame (module -> device, interactive, required) + SYSTEM_DOWNLIK = 0x11, // System downlink frame (module -> device, optional) + DEVICE_UPLINK = 0x12, // Device uplink frame (module <- device , interactive, optional) + SYSTEM_QUERY = 0x13, // System query frame (module -> device, optional) + SYSTEM_QUERY_RESPONSE = 0x14, // System query response frame (module <- device , optional) + DEVICE_QUERY = 0x15, // Device query frame (module <- device, optional) + DEVICE_QUERY_RESPONSE = 0x16, // Device query response frame (module -> device, optional) + GROUP_COMMAND = 0x60, // Group command frame (module -> device, interactive, optional) + GET_DEVICE_VERSION = 0x61, // Requests device version (module -> device, required) + GET_DEVICE_VERSION_RESPONSE = 0x62, // Device version answer (module <- device, required_ + GET_ALL_ADDRESSES = 0x67, // Requests all devices addresses (module -> device, interactive, optional) + GET_ALL_ADDRESSES_RESPONSE = + 0x68, // Answer to request of all devices addresses (module <- device , interactive, optional) + HANDSET_CHANGE_NOTIFICATION = 0x69, // Handset change notification frame (module <- device , interactive, optional) + GET_DEVICE_ID = 0x70, // Requests Device ID (module -> device, required) + GET_DEVICE_ID_RESPONSE = 0x71, // Response to device ID request (module <- device , required) + GET_ALARM_STATUS = 0x73, // Alarm status request (module -> device, required) + GET_ALARM_STATUS_RESPONSE = 0x74, // Response to alarm status request (module <- device, required) + GET_DEVICE_CONFIGURATION = 0x7C, // Requests device configuration (module -> device, interactive, required) + GET_DEVICE_CONFIGURATION_RESPONSE = + 0x7D, // Response to device configuration request (module <- device, interactive, required) + DOWNLINK_TRANSPARENT_TRANSMISSION = 0x8C, // Downlink transparent transmission (proxy data Haier cloud -> device) + // (module -> device, interactive, optional) + UPLINK_TRANSPARENT_TRANSMISSION = 0x8D, // Uplink transparent transmission (proxy data device -> Haier cloud) (module + // <- device, interactive, optional) + START_DEVICE_UPGRADE = 0xE1, // Initiate device OTA upgrade (module -> device, OTA required) + START_DEVICE_UPGRADE_RESPONSE = 0xE2, // Response to initiate device upgrade command (module <- device, OTA required) + GET_FIRMWARE_CONTENT = 0xE5, // Requests to send firmware (module <- device, OTA required) + GET_FIRMWARE_CONTENT_RESPONSE = + 0xE6, // Response to send firmware request (module -> device, OTA required) (multipacket?) + CHANGE_BAUD_RATE = 0xE7, // Requests to change port baud rate (module <- device, OTA required) + CHANGE_BAUD_RATE_RESPONSE = 0xE8, // Response to change port baud rate request (module -> device, OTA required) + GET_SUBBOARD_INFO = 0xE9, // Requests subboard information (module -> device, required) + GET_SUBBOARD_INFO_RESPONSE = 0xEA, // Response to subboard information request (module <- device, required) + GET_HARDWARE_INFO = 0xEB, // Requests information about device and subboard (module -> device, required) + GET_HARDWARE_INFO_RESPONSE = 0xEC, // Response to hardware information request (module <- device, required) + GET_UPGRADE_RESULT = 0xED, // Requests result of the firmware update (module <- device, OTA required) + GET_UPGRADE_RESULT_RESPONSE = 0xEF, // Response to firmware update results request (module -> device, OTA required) + GET_NETWORK_STATUS = 0xF0, // Requests network status (module <- device, interactive, optional) + GET_NETWORK_STATUS_RESPONSE = 0xF1, // Response to network status request (module -> device, interactive, optional) + START_WIFI_CONFIGURATION = 0xF2, // Starts WiFi configuration procedure (module <- device, interactive, required) + START_WIFI_CONFIGURATION_RESPONSE = + 0xF3, // Response to start WiFi configuration request (module -> device, interactive, required) + STOP_WIFI_CONFIGURATION = 0xF4, // Stop WiFi configuration procedure (module <- device, interactive, required) + STOP_WIFI_CONFIGURATION_RESPONSE = + 0xF5, // Response to stop WiFi configuration request (module -> device, interactive, required) + REPORT_NETWORK_STATUS = 0xF7, // Reports network status (module -> device, required) + CLEAR_CONFIGURATION = 0xF8, // Request to clear module configuration (module <- device, interactive, optional) + BIG_DATA_REPORT_CONFIGURATION = + 0xFA, // Configuration for autoreport device full status (module -> device, interactive, optional) + BIG_DATA_REPORT_CONFIGURATION_RESPONSE = + 0xFB, // Response to set big data configuration (module <- device, interactive, optional) + GET_MANAGEMENT_INFORMATION = 0xFC, // Request management information from device (module -> device, required) + GET_MANAGEMENT_INFORMATION_RESPONSE = + 0xFD, // Response to management information request (module <- device, required) + WAKE_UP = 0xFE, // Request to wake up (module <-> device, optional) +}; + +enum class SubcomandsControl : uint16_t { + GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) + GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None) + GET_BIG_DATA = 0x4DFE, // Request big data information from device (packet content: None) + SET_PARAMETERS = 0x5C01, // Set parameters of the device and device return parameters (packet content: parameter ID1 + // + parameter data1 + parameter ID2 + parameter data 2 + ...) + SET_SINGLE_PARAMETER = 0x5D00, // Set single parameter (0x5DXX second byte define parameter ID) and return all user + // data (packet content: ???) + SET_GROUP_PARAMETERS = 0x6001, // Set group parameters to device (0x60XX second byte define parameter is group ID, + // the only group mentioned in document is 1) and return all user data (packet + // content: all values like in status packet) +}; + +} // namespace hon_protocol +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/logger_handler.cpp b/esphome/components/haier/logger_handler.cpp new file mode 100644 index 0000000000..f886318097 --- /dev/null +++ b/esphome/components/haier/logger_handler.cpp @@ -0,0 +1,33 @@ +#include "logger_handler.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace haier { + +void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const char *message) { + switch (level) { + case haier_protocol::HaierLogLevel::LEVEL_ERROR: + esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_WARNING: + esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_INFO: + esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_DEBUG: + esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_VERBOSE: + esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, "%s", message); + break; + default: + // Just ignore everything else + break; + } +} + +void init_haier_protocol_logging() { haier_protocol::set_log_handler(esphome::haier::esphome_logger); }; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/logger_handler.h b/esphome/components/haier/logger_handler.h new file mode 100644 index 0000000000..2955468f37 --- /dev/null +++ b/esphome/components/haier/logger_handler.h @@ -0,0 +1,14 @@ +#pragma once + +// HaierProtocol +#include + +namespace esphome { +namespace haier { + +// This file is called in the code generated by python script +// Do not use it directly! +void init_haier_protocol_logging(); + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp new file mode 100644 index 0000000000..9c0fbac350 --- /dev/null +++ b/esphome/components/haier/smartair2_climate.cpp @@ -0,0 +1,457 @@ +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "smartair2_climate.h" +#include "smartair2_packet.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; + +Smartair2Climate::Smartair2Climate() + : last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]) { + this->traits_.set_supported_presets({ + climate::CLIMATE_PRESET_NONE, + climate::CLIMATE_PRESET_BOOST, + climate::CLIMATE_PRESET_COMFORT, + }); +} + +haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type, + (uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + result = this->process_status_message_(data, data_size); + if (result != haier_protocol::HandlerError::HANDLER_OK) { + ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + } else { + if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { + memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); + } else { + ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, + sizeof(smartair2_protocol::HaierPacketControl)); + } + if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) { + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { + this->set_phase_(ProtocolPhases::IDLE); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + } + } + return result; + } else { + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + return result; + } +} + +void Smartair2Climate::set_answers_handlers() { + this->haier_protocol_.set_answer_handler( + (uint8_t) (smartair2_protocol::FrameType::CONTROL), + std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); +} + +void Smartair2Climate::dump_config() { + HaierClimateBase::dump_config(); + ESP_LOGCONFIG(TAG, " Protocol version: smartAir2"); +} + +void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_INIT_1: + this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + break; + case ProtocolPhases::WAITING_ANSWER_INIT_1: + case ProtocolPhases::SENDING_INIT_2: + case ProtocolPhases::WAITING_ANSWER_INIT_2: + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + break; + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + this->set_phase_(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, + 0x4D01); + this->send_message_(STATUS_REQUEST, false); + this->last_status_request_ = now; + this->set_phase_(ProtocolPhases::WAITING_FIRST_STATUS_ANSWER); + } + break; + case ProtocolPhases::SENDING_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, + 0x4D01); + this->send_message_(STATUS_REQUEST, false); + this->last_status_request_ = now; + this->set_phase_(ProtocolPhases::WAITING_STATUS_ANSWER); + } + break; + case ProtocolPhases::SENDING_CONTROL: + if (this->first_control_attempt_) { + this->control_request_timestamp_ = now; + this->first_control_attempt_ = false; + } + if (this->is_control_message_timeout_exceeded_(now)) { + ESP_LOGW(TAG, "Sending control packet timeout!"); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + this->forced_request_status_ = true; + this->forced_publish_ = true; + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->can_send_message() && this->is_control_message_interval_exceeded_( + now)) // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests + { + haier_protocol::HaierMessage control_message = get_control_message(); + this->send_message_(control_message, false); + ESP_LOGI(TAG, "Control packet sent"); + this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER); + } + break; + case ProtocolPhases::SENDING_POWER_ON_COMMAND: + case ProtocolPhases::SENDING_POWER_OFF_COMMAND: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + haier_protocol::HaierMessage power_cmd( + (uint8_t) smartair2_protocol::FrameType::CONTROL, + this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03); + this->send_message_(power_cmd, false); + this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND + ? ProtocolPhases::WAITING_POWER_ON_ANSWER + : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + } + break; + case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: + case ProtocolPhases::WAITING_STATUS_ANSWER: + case ProtocolPhases::WAITING_CONTROL_ANSWER: + case ProtocolPhases::WAITING_POWER_ON_ANSWER: + case ProtocolPhases::WAITING_POWER_OFF_ANSWER: + break; + case ProtocolPhases::IDLE: { + if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { + this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST); + this->forced_request_status_ = false; + } + } break; + default: + // Shouldn't get here + ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); + this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + break; + } +} + +haier_protocol::HaierMessage Smartair2Climate::get_control_message() { + uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl)); + smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer; + out_data->cntrl = 0; + if (this->hvac_settings_.valid) { + HvacSettings climate_control; + climate_control = this->hvac_settings_; + if (climate_control.mode.has_value()) { + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + out_data->ac_power = 0; + break; + + case CLIMATE_MODE_AUTO: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + + case CLIMATE_MODE_HEAT: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + + case CLIMATE_MODE_DRY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + + case CLIMATE_MODE_FAN_ONLY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN; + out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode + break; + + case CLIMATE_MODE_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Set fan speed, if we are in fan mode, reject auto in fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (this->mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + } + // Set swing mode + if (climate_control.swing_mode.has_value()) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->use_swing_bits = 0; + out_data->swing_both = 0; + break; + case CLIMATE_SWING_VERTICAL: + out_data->swing_both = 0; + out_data->vertical_swing = 1; + out_data->horizontal_swing = 0; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->swing_both = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 1; + break; + case CLIMATE_SWING_BOTH: + out_data->swing_both = 1; + out_data->use_swing_bits = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 0; + break; + } + } + if (climate_control.target_temperature.has_value()) { + out_data->set_point = + climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16. + } + if (out_data->ac_power == 0) { + // If AC is off - no presets alowed + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; + case CLIMATE_PRESET_BOOST: + out_data->turbo_mode = 1; + out_data->quiet_mode = 0; + break; + case CLIMATE_PRESET_COMFORT: + out_data->turbo_mode = 0; + out_data->quiet_mode = 1; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; + } + } + } + out_data->display_status = this->display_status_ ? 0 : 1; + out_data->health_mode = this->health_mode_ ? 1 : 0; + return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, + sizeof(smartair2_protocol::HaierPacketControl)); +} + +haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { + if (size < sizeof(smartair2_protocol::HaierStatus)) + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + smartair2_protocol::HaierStatus packet; + memcpy(&packet, packet_buffer, size); + bool should_publish = false; + { + // Extra modes/presets + optional old_preset = this->preset; + if (packet.control.turbo_mode != 0) { + this->preset = CLIMATE_PRESET_BOOST; + } else if (packet.control.quiet_mode != 0) { + this->preset = CLIMATE_PRESET_COMFORT; + } else { + this->preset = CLIMATE_PRESET_NONE; + } + should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + } + { + // Target temperature + float old_target_temperature = this->target_temperature; + this->target_temperature = packet.control.set_point + 16.0f; + should_publish = should_publish || (old_target_temperature != this->target_temperature); + } + { + // Current temperature + float old_current_temperature = this->current_temperature; + this->current_temperature = packet.control.room_temperature; + should_publish = should_publish || (old_current_temperature != this->current_temperature); + } + { + // Fan mode + optional old_fan_mode = this->fan_mode; + // remember the fan speed we last had for climate vs fan + if (packet.control.ac_mode == (uint8_t) smartair2_protocol::ConditioningMode::FAN) { + if (packet.control.fan_mode != (uint8_t) smartair2_protocol::FanMode::FAN_AUTO) + this->fan_mode_speed_ = packet.control.fan_mode; + } else { + this->other_modes_fan_speed_ = packet.control.fan_mode; + } + switch (packet.control.fan_mode) { + case (uint8_t) smartair2_protocol::FanMode::FAN_AUTO: + // Somtimes AC reports in fan only mode that fan speed is auto + // but never accept this value back + if (packet.control.ac_mode != (uint8_t) smartair2_protocol::ConditioningMode::FAN) { + this->fan_mode = CLIMATE_FAN_AUTO; + } else { + should_publish = true; + } + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_MID: + this->fan_mode = CLIMATE_FAN_MEDIUM; + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_LOW: + this->fan_mode = CLIMATE_FAN_LOW; + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_HIGH: + this->fan_mode = CLIMATE_FAN_HIGH; + break; + } + should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + } + { + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status == 0; + if (disp_status != this->display_status_) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->set_force_send_control_(true); + } else { + this->display_status_ = disp_status; + } + } + } + } + { + // Climate mode + ClimateMode old_mode = this->mode; + if (packet.control.ac_power == 0) { + this->mode = CLIMATE_MODE_OFF; + } else { + // Check current hvac mode + switch (packet.control.ac_mode) { + case (uint8_t) smartair2_protocol::ConditioningMode::COOL: + this->mode = CLIMATE_MODE_COOL; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::HEAT: + this->mode = CLIMATE_MODE_HEAT; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::DRY: + this->mode = CLIMATE_MODE_DRY; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::FAN: + this->mode = CLIMATE_MODE_FAN_ONLY; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::AUTO: + this->mode = CLIMATE_MODE_AUTO; + break; + } + } + should_publish = should_publish || (old_mode != this->mode); + } + { + // Health mode + bool old_health_mode = this->health_mode_; + this->health_mode_ = packet.control.health_mode == 1; + should_publish = should_publish || (old_health_mode != this->health_mode_); + } + { + // Swing mode + ClimateSwingMode old_swing_mode = this->swing_mode; + if (packet.control.swing_both == 0) { + if (packet.control.vertical_swing != 0) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else if (packet.control.horizontal_swing != 0) { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } else { + swing_mode = CLIMATE_SWING_BOTH; + } + should_publish = should_publish || (old_swing_mode != this->swing_mode); + } + this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); + if (this->forced_publish_ || should_publish) { +#if (HAIER_LOG_LEVEL > 4) + std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); +#endif + this->publish_state(); +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGV(TAG, "Publish delay: %lld ms", + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - + _publish_start) + .count()); +#endif + this->forced_publish_ = false; + } + if (should_publish) { + ESP_LOGI(TAG, "HVAC values changed"); + } + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Vertical Swing Status = 0x%X", packet.control.vertical_swing); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Set Point Status = 0x%X", packet.control.set_point); + return haier_protocol::HandlerError::HANDLER_OK; +} + +bool Smartair2Climate::is_message_invalid(uint8_t message_type) { + return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID; +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.h b/esphome/components/haier/smartair2_climate.h new file mode 100644 index 0000000000..c89d1f0be9 --- /dev/null +++ b/esphome/components/haier/smartair2_climate.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include "haier_base.h" + +namespace esphome { +namespace haier { + +class Smartair2Climate : public HaierClimateBase { + public: + Smartair2Climate(); + Smartair2Climate(const Smartair2Climate &) = delete; + Smartair2Climate &operator=(const Smartair2Climate &) = delete; + ~Smartair2Climate(); + void dump_config() override; + + protected: + void set_answers_handlers() override; + void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_control_message() override; + bool is_message_invalid(uint8_t message_type) override; + // Answers handlers + haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + size_t data_size); + // Helper functions + haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); + std::unique_ptr last_status_message_; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_packet.h b/esphome/components/haier/smartair2_packet.h new file mode 100644 index 0000000000..8046516c5f --- /dev/null +++ b/esphome/components/haier/smartair2_packet.h @@ -0,0 +1,97 @@ +#pragma once + +namespace esphome { +namespace haier { +namespace smartair2_protocol { + +enum class ConditioningMode : uint8_t { AUTO = 0x00, COOL = 0x01, HEAT = 0x02, FAN = 0x03, DRY = 0x04 }; + +enum class FanMode : uint8_t { FAN_HIGH = 0x00, FAN_MID = 0x01, FAN_LOW = 0x02, FAN_AUTO = 0x03 }; + +struct HaierPacketControl { + // Control bytes starts here + // 10 + uint8_t : 8; // Temperature high byte + // 11 + uint8_t room_temperature; // current room temperature 1°C step + // 12 + uint8_t : 8; // Humidity high byte + // 13 + uint8_t room_humidity; // Humidity 0%-100% with 1% step + // 14 + uint8_t : 8; + // 15 + uint8_t cntrl; // In AC => ESP packets - 0x7F, in ESP => AC packets - 0x00 + // 16 + uint8_t : 8; + // 17 + uint8_t : 8; + // 18 + uint8_t : 8; + // 19 + uint8_t : 8; + // 20 + uint8_t : 8; + // 21 + uint8_t ac_mode; // See enum ConditioningMode + // 22 + uint8_t : 8; + // 23 + uint8_t fan_mode; // See enum FanMode + // 24 + uint8_t : 8; + // 25 + uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define + // vertical/horizontal/off + // 26 + uint8_t : 3; + uint8_t use_fahrenheit : 1; + uint8_t : 3; + uint8_t lock_remote : 1; // Disable remote + // 27 + uint8_t ac_power : 1; // Is ac on or off + uint8_t : 2; + uint8_t health_mode : 1; // Health mode on or off + uint8_t compressor : 1; // Compressor on or off ??? + uint8_t : 1; + uint8_t ten_degree : 1; // 10 degree status (only work in heat mode) + uint8_t : 0; + // 28 + uint8_t : 8; + // 29 + uint8_t use_swing_bits : 1; // Indicate if horizontal_swing and vertical_swing should be used + uint8_t turbo_mode : 1; // Turbo mode + uint8_t quiet_mode : 1; // Sleep mode + uint8_t horizontal_swing : 1; // Horizontal swing (if swing_both == 0) + uint8_t vertical_swing : 1; // Vertical swing (if swing_both == 0) if vertical_swing and horizontal_swing both 0 => + // swing off + uint8_t display_status : 1; // Led on or off + uint8_t : 0; + // 30 + uint8_t : 8; + // 31 + uint8_t : 8; + // 32 + uint8_t : 8; // Target temperature high byte + // 33 + uint8_t set_point; // Target temperature with 16°C offset, 1°C step +}; + +struct HaierStatus { + uint16_t subcommand; + HaierPacketControl control; +}; + +enum class FrameType : uint8_t { + CONTROL = 0x01, + STATUS = 0x02, + INVALID = 0x03, + CONFIRM = 0x05, + GET_DEVICE_VERSION = 0x61, + REPORT_NETWORK_STATUS = 0xF7, + NO_COMMAND = 0xFF, +}; + +} // namespace smartair2_protocol +} // namespace haier +} // namespace esphome diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index fdc9fd1ddf..2b2190d28b 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -14,6 +14,14 @@ ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len return bus_->read(address_, data, len); } +ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { + a_register = convert_big_endian(a_register); + ErrorCode const err = this->write(reinterpret_cast(&a_register), 2, stop); + if (err != ERROR_OK) + return err; + return bus_->read(address_, data, len); +} + ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { WriteBuffer buffers[2]; buffers[0].data = &a_register; @@ -23,6 +31,16 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz return bus_->writev(address_, buffers, 2, stop); } +ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) { + a_register = convert_big_endian(a_register); + WriteBuffer buffers[2]; + buffers[0].data = reinterpret_cast(&a_register); + buffers[0].len = 2; + buffers[1].data = data; + buffers[1].len = len; + return bus_->writev(address_, buffers, 2, stop); +} + bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { if (read_register(a_register, reinterpret_cast(data), len * 2) != ERROR_OK) return false; @@ -60,5 +78,26 @@ uint8_t I2CRegister::get() const { return value; } +I2CRegister16 &I2CRegister16::operator=(uint8_t value) { + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} +I2CRegister16 &I2CRegister16::operator&=(uint8_t value) { + value &= get(); + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} +I2CRegister16 &I2CRegister16::operator|=(uint8_t value) { + value |= get(); + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} + +uint8_t I2CRegister16::get() const { + uint8_t value = 0x00; + this->parent_->read_register16(this->register_, &value, 1); + return value; +} + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 780528a5c7..eb5d463b65 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -31,6 +31,25 @@ class I2CRegister { uint8_t register_; }; +class I2CRegister16 { + public: + I2CRegister16 &operator=(uint8_t value); + I2CRegister16 &operator&=(uint8_t value); + I2CRegister16 &operator|=(uint8_t value); + + explicit operator uint8_t() const { return get(); } + + uint8_t get() const; + + protected: + friend class I2CDevice; + + I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {} + + I2CDevice *parent_; + uint16_t register_; +}; + // like ntohs/htons but without including networking headers. // ("i2c" byte order is big-endian) inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } @@ -44,12 +63,15 @@ class I2CDevice { void set_i2c_bus(I2CBus *bus) { bus_ = bus; } I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; } ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); // Compat APIs diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 51688322f6..24c1860e6f 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -14,6 +14,7 @@ namespace i2c { static const char *const TAG = "i2c.idf"; void IDFI2CBus::setup() { + ESP_LOGCONFIG(TAG, "Setting up I2C bus..."); static i2c_port_t next_port = 0; port_ = next_port++; diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 29603eb30f..0435460b6a 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -13,6 +13,7 @@ from esphome.const import ( CONF_PAGES, CONF_RESET_PIN, CONF_DIMENSIONS, + CONF_DATA_RATE, ) DEPENDENCIES = ["spi"] @@ -43,6 +44,7 @@ MODELS = { "ILI9481": ili9XXX_ns.class_("ILI9XXXILI9481", ili9XXXSPI), "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), + "ILI9488_A": ili9XXX_ns.class_("ILI9XXXILI9488A", ili9XXXSPI), "ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI), "S3BOX": ili9XXX_ns.class_("ILI9XXXS3Box", ili9XXXSPI), "S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI), @@ -97,6 +99,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list( cv.file_ ), + cv.Optional(CONF_DATA_RATE, default="40MHz"): spi.SPI_DATA_RATE_SCHEMA, } ) .extend(cv.polling_component_schema("1s")) @@ -118,7 +121,7 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) @@ -175,3 +178,6 @@ async def to_code(config): if rhs is not None: prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_palette(prog_arr)) + + spi_data_rate = str(spi.SPI_DATA_RATE_OPTIONS[config[CONF_DATA_RATE]]) + cg.add_define("ILI9XXXDisplay_DATA_RATE", cg.RawExpression(spi_data_rate)) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 6fc6da3cdb..750f629db2 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -152,12 +152,10 @@ void ILI9XXXDisplay::update() { this->need_update_ = true; return; } + this->prossing_update_ = true; do { - this->prossing_update_ = true; this->need_update_ = false; - if (!this->need_update_) { - this->do_update_(); - } + this->do_update_(); } while (this->need_update_); this->prossing_update_ = false; this->display_(); @@ -411,6 +409,17 @@ void ILI9XXXILI9488::initialize() { this->is_18bitdisplay_ = true; } // 40_TFT display +void ILI9XXXILI9488A::initialize() { + this->init_lcd_(INITCMD_ILI9488_A); + if (this->width_ == 0) { + this->width_ = 480; + } + if (this->height_ == 0) { + this->height_ = 320; + } + this->is_18bitdisplay_ = true; +} +// 40_TFT display void ILI9XXXST7796::initialize() { this->init_lcd_(INITCMD_ST7796); if (this->width_ == 0) { diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index dc7bfdc6eb..15b08e6c76 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -15,10 +15,14 @@ enum ILI9XXXColorMode { BITS_16 = 0x10, }; +#ifndef ILI9XXXDisplay_DATA_RATE +#define ILI9XXXDisplay_DATA_RATE spi::DATA_RATE_40MHZ +#endif // ILI9XXXDisplay_DATA_RATE + class ILI9XXXDisplay : public PollingComponent, public display::DisplayBuffer, public spi::SPIDevice { + spi::CLOCK_PHASE_LEADING, ILI9XXXDisplay_DATA_RATE> { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; @@ -128,6 +132,12 @@ class ILI9XXXILI9488 : public ILI9XXXDisplay { void initialize() override; }; +//----------- ILI9XXX_35_TFT origin colors rotated display -------------- +class ILI9XXXILI9488A : public ILI9XXXDisplay { + protected: + void initialize() override; +}; + //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXST7796 : public ILI9XXXDisplay { protected: diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index a17e6b127c..1856fb06ab 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -139,6 +139,40 @@ static const uint8_t PROGMEM INITCMD_ILI9488[] = { + // 5 frames + //ILI9XXX_ETMOD, 1, 0xC6, // + + + ILI9XXX_SLPOUT, 0x80, // Exit sleep mode + //ILI9XXX_INVON , 0, + ILI9XXX_DISPON, 0x80, // Set display on + 0x00 // end +}; + +static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { + ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, + ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, + + ILI9XXX_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2 + ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL + ILI9XXX_VMCTR1, 3, 0x00, 0x12, 0x80, // nVM VCM_REG VCM_REG_EN + + ILI9XXX_IFMODE, 1, 0x00, + ILI9XXX_FRMCTR1, 1, 0xA0, // Frame rate = 60Hz + ILI9XXX_INVCTR, 1, 0x02, // Display Inversion Control = 2dot + + ILI9XXX_DFUNCTR, 2, 0x02, 0x02, // Nomal scan + + 0xE9, 1, 0x00, // Set Image Functio. Disable 24 bit data + + ILI9XXX_ADJCTL3, 4, 0xA9, 0x51, 0x2C, 0x82, // Adjust Control 3 + + ILI9XXX_MADCTL, 1, 0x28, + //ILI9XXX_PIXFMT, 1, 0x55, // Interface Pixel Format = 16bit + ILI9XXX_PIXFMT, 1, 0x66, //ILI9488 only supports 18-bit pixel format in 4/3 wire SPI mode + + + // 5 frames //ILI9XXX_ETMOD, 1, 0xC6, // @@ -218,12 +252,12 @@ static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control 0xF2, 1, 0x00, // 3Gamma Function Disable ILI9XXX_GAMMASET , 1, 0x01, // Gamma curve selected - ILI9XXX_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma - 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, - 0x0E, 0x09, 0x00, - ILI9XXX_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma - 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, - 0x31, 0x36, 0x0F, + ILI9XXX_GMCTRP1 , 14, 0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, // Set Gamma + 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, + 0x18, 0x1B, + ILI9XXX_GMCTRN1 , 14, 0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, // Set Gamma + 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, + 0x17, 0x1B, ILI9XXX_SLPOUT , 0x80, // Exit Sleep ILI9XXX_DISPON , 0x80, // Display on 0x00 // End of list diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index e7cf492c7b..392efb18a2 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -6,7 +6,7 @@ import re import requests from esphome import core -from esphome.components import display, font +from esphome.components import font import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import ( @@ -28,7 +28,9 @@ DOMAIN = "image" DEPENDENCIES = ["display"] MULTI_CONF = True -ImageType = display.display_ns.enum("ImageType") +image_ns = cg.esphome_ns.namespace("image") + +ImageType = image_ns.enum("ImageType") IMAGE_TYPE = { "BINARY": ImageType.IMAGE_TYPE_BINARY, "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, @@ -46,7 +48,7 @@ MDI_DOWNLOAD_TIMEOUT = 30 # seconds SOURCE_LOCAL = "local" SOURCE_MDI = "mdi" -Image_ = display.display_ns.class_("Image") +Image_ = image_ns.class_("Image") def _compute_local_icon_path(value) -> Path: diff --git a/esphome/components/display/image.cpp b/esphome/components/image/image.cpp similarity index 96% rename from esphome/components/display/image.cpp rename to esphome/components/image/image.cpp index b3cab3ff7f..0ddb8110cb 100644 --- a/esphome/components/display/image.cpp +++ b/esphome/components/image/image.cpp @@ -1,12 +1,11 @@ #include "image.h" #include "esphome/core/hal.h" -#include "display_buffer.h" namespace esphome { -namespace display { +namespace image { -void Image::draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) { +void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { switch (type_) { case IMAGE_TYPE_BINARY: { for (int img_x = 0; img_x < width_; img_x++) { @@ -131,5 +130,5 @@ ImageType Image::get_type() const { return this->type_; } Image::Image(const uint8_t *data_start, int width, int height, ImageType type) : width_(width), height_(height), type_(type), data_start_(data_start) {} -} // namespace display +} // namespace image } // namespace esphome diff --git a/esphome/components/display/image.h b/esphome/components/image/image.h similarity index 69% rename from esphome/components/display/image.h rename to esphome/components/image/image.h index ac2d5a3421..4e869f5204 100644 --- a/esphome/components/display/image.h +++ b/esphome/components/image/image.h @@ -1,8 +1,9 @@ #pragma once #include "esphome/core/color.h" +#include "esphome/components/display/display_buffer.h" namespace esphome { -namespace display { +namespace image { enum ImageType { IMAGE_TYPE_BINARY = 0, @@ -30,29 +31,15 @@ inline int image_type_to_bpp(ImageType type) { inline int image_type_to_width_stride(int width, ImageType type) { return (width * image_type_to_bpp(type) + 7u) / 8u; } -/// Turn the pixel OFF. -extern const Color COLOR_OFF; -/// Turn the pixel ON. -extern const Color COLOR_ON; - -class DisplayBuffer; - -class BaseImage { - public: - virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0; - virtual int get_width() const = 0; - virtual int get_height() const = 0; -}; - -class Image : public BaseImage { +class Image : public display::BaseImage { public: Image(const uint8_t *data_start, int width, int height, ImageType type); - Color get_pixel(int x, int y, Color color_on = COLOR_ON, Color color_off = COLOR_OFF) const; + Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const; int get_width() const override; int get_height() const override; ImageType get_type() const; - void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) override; + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; void set_transparency(bool transparent) { transparent_ = transparent; } bool has_transparency() const { return transparent_; } @@ -71,5 +58,5 @@ class Image : public BaseImage { bool transparent_; }; -} // namespace display +} // namespace image } // namespace esphome diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index a17f37c920..7534731175 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -115,7 +115,7 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index be39cc2979..47c4cdb0bd 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -112,7 +112,6 @@ CONFIG_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "ld2410", - baud_rate=256000, require_tx=True, require_rx=True, parity="NONE", diff --git a/esphome/components/mcp2515/canbus.py b/esphome/components/mcp2515/canbus.py index c410c1af69..4353cd7bc6 100644 --- a/esphome/components/mcp2515/canbus.py +++ b/esphome/components/mcp2515/canbus.py @@ -16,6 +16,7 @@ McpMode = mcp2515_ns.enum("CANCTRL_REQOP_MODE") CAN_CLOCK = { "8MHZ": CanClock.MCP_8MHZ, + "12MHZ": CanClock.MCP_12MHZ, "16MHZ": CanClock.MCP_16MHZ, "20MHZ": CanClock.MCP_20MHZ, } diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index b90b4de66d..fe4a68b583 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -16,11 +16,14 @@ const struct MCP2515::RxBnRegs MCP2515::RXB[N_RXBUFFERS] = {{MCP_RXB0CTRL, MCP_R bool MCP2515::setup_internal() { this->spi_setup(); - if (this->reset_() == canbus::ERROR_FAIL) + if (this->reset_() != canbus::ERROR_OK) return false; - this->set_bitrate_(this->bit_rate_, this->mcp_clock_); - this->set_mode_(this->mcp_mode_); - ESP_LOGV(TAG, "setup done"); + if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK) + return false; + if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK) + return false; + uint8_t err_flags = this->get_error_flags_(); + ESP_LOGD(TAG, "mcp2515 setup done, error_flags = %02X", err_flags); return true; } @@ -38,7 +41,7 @@ canbus::Error MCP2515::reset_() { set_registers_(MCP_TXB0CTRL, zeros, 14); set_registers_(MCP_TXB1CTRL, zeros, 14); set_registers_(MCP_TXB2CTRL, zeros, 14); - ESP_LOGD(TAG, "reset() CLEARED TXB registers"); + ESP_LOGV(TAG, "reset() CLEARED TXB registers"); set_register_(MCP_RXB0CTRL, 0); set_register_(MCP_RXB1CTRL, 0); @@ -114,16 +117,12 @@ canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) { modify_register_(MCP_CANCTRL, CANCTRL_REQOP, mode); uint32_t end_time = millis() + 10; - bool mode_match = false; while (millis() < end_time) { - uint8_t new_mode = read_register_(MCP_CANSTAT); - new_mode &= CANSTAT_OPMOD; - mode_match = new_mode == mode; - if (mode_match) { - break; - } + if ((read_register_(MCP_CANSTAT) & CANSTAT_OPMOD) == mode) + return canbus::ERROR_OK; } - return mode_match ? canbus::ERROR_OK : canbus::ERROR_FAIL; + ESP_LOGE(TAG, "Failed to set mode"); + return canbus::ERROR_FAIL; } canbus::Error MCP2515::set_clk_out_(const CanClkOut divisor) { @@ -451,6 +450,78 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo } break; + case (MCP_12MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5Kbps + cfg1 = MCP_12MHZ_5KBPS_CFG1; + cfg2 = MCP_12MHZ_5KBPS_CFG2; + cfg3 = MCP_12MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10Kbps + cfg1 = MCP_12MHZ_10KBPS_CFG1; + cfg2 = MCP_12MHZ_10KBPS_CFG2; + cfg3 = MCP_12MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20Kbps + cfg1 = MCP_12MHZ_20KBPS_CFG1; + cfg2 = MCP_12MHZ_20KBPS_CFG2; + cfg3 = MCP_12MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_12MHZ_33K3BPS_CFG1; + cfg2 = MCP_12MHZ_33K3BPS_CFG2; + cfg3 = MCP_12MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_12MHZ_40KBPS_CFG1; + cfg2 = MCP_12MHZ_40KBPS_CFG2; + cfg3 = MCP_12MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg2 = MCP_12MHZ_50KBPS_CFG2; + cfg3 = MCP_12MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_12MHZ_80KBPS_CFG1; + cfg2 = MCP_12MHZ_80KBPS_CFG2; + cfg3 = MCP_12MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_12MHZ_100KBPS_CFG1; + cfg2 = MCP_12MHZ_100KBPS_CFG2; + cfg3 = MCP_12MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_12MHZ_125KBPS_CFG1; + cfg2 = MCP_12MHZ_125KBPS_CFG2; + cfg3 = MCP_12MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_12MHZ_200KBPS_CFG1; + cfg2 = MCP_12MHZ_200KBPS_CFG2; + cfg3 = MCP_12MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_12MHZ_250KBPS_CFG1; + cfg2 = MCP_12MHZ_250KBPS_CFG2; + cfg3 = MCP_12MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_12MHZ_500KBPS_CFG1; + cfg2 = MCP_12MHZ_500KBPS_CFG2; + cfg3 = MCP_12MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_12MHZ_1000KBPS_CFG1; + cfg2 = MCP_12MHZ_1000KBPS_CFG2; + cfg3 = MCP_12MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + case (MCP_16MHZ): switch (can_speed) { case (canbus::CAN_5KBPS): // 5Kbps @@ -602,6 +673,7 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo set_register_(MCP_CNF3, cfg3); // NOLINT return canbus::ERROR_OK; } else { + ESP_LOGE(TAG, "Invalid frequency/bitrate combination: %d/%d", can_clock, can_speed); return canbus::ERROR_FAIL; } } diff --git a/esphome/components/mcp2515/mcp2515.h b/esphome/components/mcp2515/mcp2515.h index 3b9797a78a..c77480ce7d 100644 --- a/esphome/components/mcp2515/mcp2515.h +++ b/esphome/components/mcp2515/mcp2515.h @@ -11,7 +11,7 @@ static const uint32_t SPI_CLOCK = 10000000; // 10MHz static const int N_TXBUFFERS = 3; static const int N_RXBUFFERS = 2; -enum CanClock { MCP_20MHZ, MCP_16MHZ, MCP_8MHZ }; +enum CanClock { MCP_20MHZ, MCP_16MHZ, MCP_12MHZ, MCP_8MHZ }; enum MASK { MASK0, MASK1 }; enum RXF { RXF0 = 0, RXF1 = 1, RXF2 = 2, RXF3 = 3, RXF4 = 4, RXF5 = 5 }; enum RXBn { RXB0 = 0, RXB1 = 1 }; diff --git a/esphome/components/mcp2515/mcp2515_defs.h b/esphome/components/mcp2515/mcp2515_defs.h index 454c760c6d..2f5cf2a238 100644 --- a/esphome/components/mcp2515/mcp2515_defs.h +++ b/esphome/components/mcp2515/mcp2515_defs.h @@ -207,6 +207,62 @@ static const uint8_t MCP_8MHZ_5KBPS_CFG1 = 0x1F; static const uint8_t MCP_8MHZ_5KBPS_CFG2 = 0xBF; static const uint8_t MCP_8MHZ_5KBPS_CFG3 = 0x87; +/* + * Speed 12M + */ + +static const uint8_t MCP_12MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_12MHZ_1000KBPS_CFG2 = 0x88; +static const uint8_t MCP_12MHZ_1000KBPS_CFG3 = 0x81; + +static const uint8_t MCP_12MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_12MHZ_500KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_500KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_250KBPS_CFG1 = 0x01; +static const uint8_t MCP_12MHZ_250KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_250KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_12MHZ_200KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_200KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_12MHZ_125KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_125KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_100KBPS_CFG1 = 0x03; +static const uint8_t MCP_12MHZ_100KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_100KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_80KBPS_CFG1 = 0x04; +static const uint8_t MCP_12MHZ_80KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_80KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_50KBPS_CFG1 = 0x07; +static const uint8_t MCP_12MHZ_50KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_50KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_40KBPS_CFG1 = 0x09; +static const uint8_t MCP_12MHZ_40KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_40KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_33K3BPS_CFG1 = 0x08; +static const uint8_t MCP_12MHZ_33K3BPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_33K3BPS_CFG3 = 0x84; + +static const uint8_t MCP_12MHZ_20KBPS_CFG1 = 0x0E; +static const uint8_t MCP_12MHZ_20KBPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_20KBPS_CFG3 = 0x84; + +static const uint8_t MCP_12MHZ_10KBPS_CFG1 = 0x31; +static const uint8_t MCP_12MHZ_10KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_10KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_5KBPS_CFG1 = 0x3B; +static const uint8_t MCP_12MHZ_5KBPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_5KBPS_CFG3 = 0x84; + /* * speed 16M */ diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index ae7b646b9d..67e749c68b 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -54,16 +54,16 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const auto &manu_datas = device.get_manufacturer_datas(); if (manu_datas.size() != 1) { - ESP_LOGE(TAG, "%s: Unexpected manu_datas size (%d)", device.address_str().c_str(), manu_datas.size()); + ESP_LOGE(TAG, "[%s] Unexpected manu_datas size (%d)", device.address_str().c_str(), manu_datas.size()); return false; } const auto &manu_data = manu_datas[0]; - ESP_LOGVV(TAG, "%s: Manufacturer data: %s", device.address_str().c_str(), format_hex_pretty(manu_data.data).c_str()); + ESP_LOGVV(TAG, "[%s] Manufacturer data: %s", device.address_str().c_str(), format_hex_pretty(manu_data.data).c_str()); if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { - ESP_LOGE(TAG, "%s: Unexpected manu_data size (%d)", device.address_str().c_str(), manu_data.data.size()); + ESP_LOGE(TAG, "[%s] Unexpected manu_data size (%d)", device.address_str().c_str(), manu_data.data.size()); return false; } @@ -72,20 +72,20 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL) { - ESP_LOGE(TAG, "%s: Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); + ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); return false; } - ESP_LOGVV(TAG, "%s: Sensor slow update rate: %d", device.address_str().c_str(), mopeka_data->slow_update_rate); - ESP_LOGVV(TAG, "%s: Sensor sync pressed: %d", device.address_str().c_str(), mopeka_data->sync_pressed); + ESP_LOGVV(TAG, "[%s] Sensor slow update rate: %d", device.address_str().c_str(), mopeka_data->slow_update_rate); + ESP_LOGVV(TAG, "[%s] Sensor sync pressed: %d", device.address_str().c_str(), mopeka_data->sync_pressed); for (u_int8_t i = 0; i < 3; i++) { - ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 1, + ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 1, mopeka_data->val[i].value_0, mopeka_data->val[i].time_0); - ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 2, + ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 2, mopeka_data->val[i].value_1, mopeka_data->val[i].time_1); - ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 3, + ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 3, mopeka_data->val[i].value_2, mopeka_data->val[i].time_2); - ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 4, + ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 4, mopeka_data->val[i].value_3, mopeka_data->val[i].time_3); } @@ -146,19 +146,19 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // This value is better than a previous one. best_value = measurements_value[i]; best_time = measurement_time; - // Reset measurement_time or next values. - measurement_time = 0; } + // Reset measurement_time or next values. + measurement_time = 0; } } } - ESP_LOGV(TAG, "%s: Found %u values with best data %u time %u.", device.address_str().c_str(), + ESP_LOGV(TAG, "[%s] Found %u values with best data %u time %u.", device.address_str().c_str(), number_of_usable_values, best_value, best_time); - if (number_of_usable_values < 2 || best_value < 2 || best_time < 2) { + if (number_of_usable_values < 1 || best_value < 2 || best_time < 2) { // At least two measurement values must be present. - ESP_LOGW(TAG, "%s: Poor read quality. Setting distance to 0.", device.address_str().c_str()); + ESP_LOGW(TAG, "[%s] Poor read quality. Setting distance to 0.", device.address_str().c_str()); if (this->distance_ != nullptr) { this->distance_->publish_state(0); } @@ -167,7 +167,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) } } else { float lpg_speed_of_sound = this->get_lpg_speed_of_sound_(temp_in_c); - ESP_LOGV(TAG, "%s: Speed of sound in current fluid %f m/s", device.address_str().c_str(), lpg_speed_of_sound); + ESP_LOGV(TAG, "[%s] Speed of sound in current fluid %f m/s", device.address_str().c_str(), lpg_speed_of_sound); uint32_t distance_value = lpg_speed_of_sound * best_time / 100.0f; diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index 51e3ec2383..64fcd3a2a8 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -77,7 +77,7 @@ void MPU6050Component::setup() { accel_config &= 0b11100111; accel_config |= (MPU6050_RANGE_2G << 3); ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config)); - if (!this->write_byte(MPU6050_REGISTER_GYRO_CONFIG, gyro_config)) { + if (!this->write_byte(MPU6050_REGISTER_ACCEL_CONFIG, accel_config)) { this->mark_failed(); return; } diff --git a/esphome/components/mqtt/mqtt_backend_idf.cpp b/esphome/components/mqtt/mqtt_backend_idf.cpp index 812e36d522..7a7aca3fa6 100644 --- a/esphome/components/mqtt/mqtt_backend_idf.cpp +++ b/esphome/components/mqtt/mqtt_backend_idf.cpp @@ -11,6 +11,7 @@ namespace mqtt { static const char *const TAG = "mqtt.idf"; bool MQTTBackendIDF::initialize_() { +#if ESP_IDF_VERSION_MAJOR < 5 mqtt_cfg_.user_context = (void *) this; mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE; @@ -47,6 +48,41 @@ bool MQTTBackendIDF::initialize_() { } else { mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP; } +#else + mqtt_cfg_.broker.address.hostname = this->host_.c_str(); + mqtt_cfg_.broker.address.port = this->port_; + mqtt_cfg_.session.keepalive = this->keep_alive_; + mqtt_cfg_.session.disable_clean_session = !this->clean_session_; + + if (!this->username_.empty()) { + mqtt_cfg_.credentials.username = this->username_.c_str(); + if (!this->password_.empty()) { + mqtt_cfg_.credentials.authentication.password = this->password_.c_str(); + } + } + + if (!this->lwt_topic_.empty()) { + mqtt_cfg_.session.last_will.topic = this->lwt_topic_.c_str(); + this->mqtt_cfg_.session.last_will.qos = this->lwt_qos_; + this->mqtt_cfg_.session.last_will.retain = this->lwt_retain_; + + if (!this->lwt_message_.empty()) { + mqtt_cfg_.session.last_will.msg = this->lwt_message_.c_str(); + mqtt_cfg_.session.last_will.msg_len = this->lwt_message_.size(); + } + } + + if (!this->client_id_.empty()) { + mqtt_cfg_.credentials.client_id = this->client_id_.c_str(); + } + if (ca_certificate_.has_value()) { + mqtt_cfg_.broker.verification.certificate = ca_certificate_.value().c_str(); + mqtt_cfg_.broker.verification.skip_cert_common_name_check = skip_cert_cn_check_; + mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_SSL; + } else { + mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_TCP; + } +#endif auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_); if (mqtt_client) { handler_.reset(mqtt_client); @@ -78,9 +114,8 @@ void MQTTBackendIDF::mqtt_event_handler_(const Event &event) { case MQTT_EVENT_CONNECTED: ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED"); - // TODO session present check this->is_connected_ = true; - this->on_connect_.call(!mqtt_cfg_.disable_clean_session); + this->on_connect_.call(event.session_present); break; case MQTT_EVENT_DISCONNECTED: ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED"); diff --git a/esphome/components/mqtt/mqtt_backend_idf.h b/esphome/components/mqtt/mqtt_backend_idf.h index 900ee9709b..9c7a5f80e9 100644 --- a/esphome/components/mqtt/mqtt_backend_idf.h +++ b/esphome/components/mqtt/mqtt_backend_idf.h @@ -22,6 +22,7 @@ struct Event { bool retain; int qos; bool dup; + bool session_present; esp_mqtt_error_codes_t error_handle; // Construct from esp_mqtt_event_t @@ -36,6 +37,7 @@ struct Event { retain(event.retain), qos(event.qos), dup(event.dup), + session_present(event.session_present), error_handle(*event.error_handle) {} }; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index cf228efd1b..b663d7751d 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -118,7 +118,7 @@ bool MQTTComponent::send_discovery_() { } else { if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; - sprintf(friendly_name_hash, "%08x", fnv1_hash(this->friendly_name())); + sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); friendly_name_hash[8] = 0; // ensure the hash-string ends with null root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; } else { diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 4946cfb924..fff75a3c00 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -1,3 +1,4 @@ +#include #include "mqtt_sensor.h" #include "esphome/core/log.h" @@ -26,7 +27,7 @@ void MQTTSensorComponent::setup() { void MQTTSensorComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Sensor '%s':", this->sensor_->get_name().c_str()); if (this->get_expire_after() > 0) { - ESP_LOGCONFIG(TAG, " Expire After: %us", this->get_expire_after() / 1000); + ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000); } LOG_MQTT_COMPONENT(true, false) } diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index d0184a58d3..b4c8f432cf 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -52,6 +52,6 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/pcf8563/__init__.py b/esphome/components/pcf8563/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp new file mode 100644 index 0000000000..f2a82735c5 --- /dev/null +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -0,0 +1,109 @@ +#include "pcf8563.h" +#include "esphome/core/log.h" + +// Datasheet: +// - https://nl.mouser.com/datasheet/2/302/PCF8563-1127619.pdf + +namespace esphome { +namespace pcf8563 { + +static const char *const TAG = "PCF8563"; + +void PCF8563Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up PCF8563..."); + if (!this->read_rtc_()) { + this->mark_failed(); + } +} + +void PCF8563Component::update() { this->read_time(); } + +void PCF8563Component::dump_config() { + ESP_LOGCONFIG(TAG, "PCF8563:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PCF8563 failed!"); + } + ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); +} + +float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } + +void PCF8563Component::read_time() { + if (!this->read_rtc_()) { + return; + } + if (pcf8563_.reg.stop) { + ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); + return; + } + ESPTime rtc_time{ + .second = uint8_t(pcf8563_.reg.second + 10 * pcf8563_.reg.second_10), + .minute = uint8_t(pcf8563_.reg.minute + 10u * pcf8563_.reg.minute_10), + .hour = uint8_t(pcf8563_.reg.hour + 10u * pcf8563_.reg.hour_10), + .day_of_week = uint8_t(pcf8563_.reg.weekday), + .day_of_month = uint8_t(pcf8563_.reg.day + 10u * pcf8563_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(pcf8563_.reg.month + 10u * pcf8563_.reg.month_10), + .year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000), + .is_dst = false, // not used + .timestamp = 0, // overwritten by recalc_timestamp_utc(false) + }; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void PCF8563Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + pcf8563_.reg.year = (now.year - 2000) % 10; + pcf8563_.reg.year_10 = (now.year - 2000) / 10 % 10; + pcf8563_.reg.month = now.month % 10; + pcf8563_.reg.month_10 = now.month / 10; + pcf8563_.reg.day = now.day_of_month % 10; + pcf8563_.reg.day_10 = now.day_of_month / 10; + pcf8563_.reg.weekday = now.day_of_week; + pcf8563_.reg.hour = now.hour % 10; + pcf8563_.reg.hour_10 = now.hour / 10; + pcf8563_.reg.minute = now.minute % 10; + pcf8563_.reg.minute_10 = now.minute / 10; + pcf8563_.reg.second = now.second % 10; + pcf8563_.reg.second_10 = now.second / 10; + pcf8563_.reg.stop = false; + + this->write_rtc_(); +} + +bool PCF8563Component::read_rtc_() { + if (!this->read_bytes(0, this->pcf8563_.raw, sizeof(this->pcf8563_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + return false; + } + ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u STOP:%s CLKOUT:%0u", pcf8563_.reg.hour_10, + pcf8563_.reg.hour, pcf8563_.reg.minute_10, pcf8563_.reg.minute, pcf8563_.reg.second_10, pcf8563_.reg.second, + pcf8563_.reg.year_10, pcf8563_.reg.year, pcf8563_.reg.month_10, pcf8563_.reg.month, pcf8563_.reg.day_10, + pcf8563_.reg.day, ONOFF(!pcf8563_.reg.stop), pcf8563_.reg.clkout_enabled); + + return true; +} + +bool PCF8563Component::write_rtc_() { + if (!this->write_bytes(0, this->pcf8563_.raw, sizeof(this->pcf8563_.raw))) { + ESP_LOGE(TAG, "Can't write I2C data."); + return false; + } + ESP_LOGD(TAG, "Write %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u OSC:%s CLKOUT:%0u", pcf8563_.reg.hour_10, + pcf8563_.reg.hour, pcf8563_.reg.minute_10, pcf8563_.reg.minute, pcf8563_.reg.second_10, pcf8563_.reg.second, + pcf8563_.reg.year_10, pcf8563_.reg.year, pcf8563_.reg.month_10, pcf8563_.reg.month, pcf8563_.reg.day_10, + pcf8563_.reg.day, ONOFF(!pcf8563_.reg.stop), pcf8563_.reg.clkout_enabled); + return true; +} +} // namespace pcf8563 +} // namespace esphome diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h new file mode 100644 index 0000000000..b6832efe72 --- /dev/null +++ b/esphome/components/pcf8563/pcf8563.h @@ -0,0 +1,124 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace pcf8563 { + +class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + void read_time(); + void write_time(); + + protected: + bool read_rtc_(); + bool write_rtc_(); + union PCF8563Reg { + struct { + // Control_1 register + bool : 3; + bool power_on_reset : 1; + bool : 1; + bool stop : 1; + bool : 1; + bool ext_test : 1; + + // Control_2 register + bool time_int : 1; + bool alarm_int : 1; + bool timer_flag : 1; + bool alarm_flag : 1; + bool timer_int_timer_pulse : 1; + bool : 3; + + // Seconds register + uint8_t second : 4; + uint8_t second_10 : 3; + bool clock_int : 1; + + // Minutes register + uint8_t minute : 4; + uint8_t minute_10 : 3; + uint8_t : 1; + + // Hours register + uint8_t hour : 4; + uint8_t hour_10 : 2; + uint8_t : 2; + + // Days register + uint8_t day : 4; + uint8_t day_10 : 2; + uint8_t : 2; + + // Weekdays register + uint8_t weekday : 3; + uint8_t unused_3 : 5; + + // Months register + uint8_t month : 4; + uint8_t month_10 : 1; + uint8_t : 2; + uint8_t century : 1; + + // Years register + uint8_t year : 4; + uint8_t year_10 : 4; + + // Minute Alarm register + uint8_t minute_alarm : 4; + uint8_t minute_alarm_10 : 3; + bool minute_alarm_enabled : 1; + + // Hour Alarm register + uint8_t hour_alarm : 4; + uint8_t hour_alarm_10 : 2; + uint8_t : 1; + bool hour_alarm_enabled : 1; + + // Day Alarm register + uint8_t day_alarm : 4; + uint8_t day_alarm_10 : 2; + uint8_t : 1; + bool day_alarm_enabled : 1; + + // Weekday Alarm register + uint8_t weekday_alarm : 3; + uint8_t : 4; + bool weekday_alarm_enabled : 1; + + // CLKout control register + uint8_t frequency_output : 2; + uint8_t : 5; + uint8_t clkout_enabled : 1; + + // Timer control register + uint8_t timer_source_frequency : 2; + uint8_t : 5; + uint8_t timer_enabled : 1; + + // Timer register + uint8_t countdown_period : 8; + + } reg; + mutable uint8_t raw[sizeof(reg)]; + } pcf8563_; +}; + +template class WriteAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->read_time(); } +}; +} // namespace pcf8563 +} // namespace esphome diff --git a/esphome/components/pcf8563/time.py b/esphome/components/pcf8563/time.py new file mode 100644 index 0000000000..2e6456a72d --- /dev/null +++ b/esphome/components/pcf8563/time.py @@ -0,0 +1,62 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome import automation +from esphome.components import i2c, time +from esphome.const import CONF_ID + +CODEOWNERS = ["@KoenBreeman"] + +DEPENDENCIES = ["i2c"] + + +pcf8563_ns = cg.esphome_ns.namespace("pcf8563") +pcf8563Component = pcf8563_ns.class_( + "PCF8563Component", time.RealTimeClock, i2c.I2CDevice +) +WriteAction = pcf8563_ns.class_("WriteAction", automation.Action) +ReadAction = pcf8563_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(pcf8563Component), + } +).extend(i2c.i2c_device_schema(0xA3)) + + +@automation.register_action( + "pcf8563.write_time", + WriteAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(pcf8563Component), + } + ), +) +async def pcf8563_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "pcf8563.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(pcf8563Component), + } + ), +) +async def pcf8563_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index afc2d91000..30f6038325 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -29,7 +29,7 @@ float PIDController::update(float setpoint, float process_value) { bool PIDController::in_deadband() { // return (fabs(error) < deadband_threshold); float err = -error_; - return ((err > 0 && err < threshold_high_) || (err < 0 && err > threshold_low_)); + return (threshold_low_ < err && err < threshold_high_); } void PIDController::calculate_proportional_term_() { diff --git a/esphome/components/qr_code/qr_code.cpp b/esphome/components/qr_code/qr_code.cpp index a2efbdb804..aecf7628dc 100644 --- a/esphome/components/qr_code/qr_code.cpp +++ b/esphome/components/qr_code/qr_code.cpp @@ -1,5 +1,5 @@ #include "qr_code.h" -#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display.h" #include "esphome/core/color.h" #include "esphome/core/log.h" @@ -33,7 +33,7 @@ void QrCode::generate_qr_code() { } } -void QrCode::draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale) { +void QrCode::draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale) { ESP_LOGV(TAG, "Drawing QR code at (%d, %d)", x_offset, y_offset); if (this->needs_update_) { diff --git a/esphome/components/qr_code/qr_code.h b/esphome/components/qr_code/qr_code.h index 58f3a70321..d88e0aa09a 100644 --- a/esphome/components/qr_code/qr_code.h +++ b/esphome/components/qr_code/qr_code.h @@ -9,13 +9,13 @@ namespace esphome { // forward declare DisplayBuffer namespace display { -class DisplayBuffer; +class Display; } // namespace display namespace qr_code { class QrCode : public Component { public: - void draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale); + void draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale); void dump_config() override; diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 030d586626..dafafc531c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -62,19 +62,19 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 6, 4) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 3, 0) # The platformio/raspberrypi version to use for arduino frameworks # - https://github.com/platformio/platform-raspberrypi/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi -ARDUINO_PLATFORM_VERSION = cv.Version(1, 7, 0) +ARDUINO_PLATFORM_VERSION = cv.Version(1, 9, 0) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(2, 6, 4), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(2, 6, 4), None), + "dev": (cv.Version(3, 3, 0), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(3, 3, 0), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 6337d89bcd..78b23e7b5e 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -33,6 +33,7 @@ SCRIPT_MODES = { PARAMETER_TYPE_TRANSLATIONS = { "string": "std::string", + "boolean": "bool", } @@ -149,6 +150,16 @@ async def to_code(config): ), ) async def script_execute_action_to_code(config, action_id, template_arg, args): + def convert(type: str): + def converter(value): + if type == "std::string": + return value + if type == "bool": + return cg.RawExpression(str(value).lower()) + return cg.RawExpression(str(value)) + + return converter + async def get_ordered_args(config, script_params): config_args = config.copy() config_args.pop(CONF_ID) @@ -160,7 +171,9 @@ async def script_execute_action_to_code(config, action_id, template_arg, args): raise EsphomeError( f"Missing parameter: '{name}' in script.execute {config[CONF_ID]}" ) - arg = await cg.templatable(config_args[name], args, type) + arg = await cg.templatable( + config_args[name], args, type, convert(str(type)) + ) script_args.append(arg) return script_args diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 06b96171a7..caaffd9701 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -217,6 +217,7 @@ OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter) ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) +TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) @@ -536,6 +537,15 @@ async def heartbeat_filter_to_code(config, filter_id): return var +@FILTER_REGISTRY.register( + "timeout", TimeoutFilter, cv.positive_time_period_milliseconds +) +async def timeout_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id, config) + await cg.register_component(var, {}) + return var + + @FILTER_REGISTRY.register( "debounce", DebounceFilter, cv.positive_time_period_milliseconds ) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 472649ebdc..ccefa556b6 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -373,6 +373,17 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { this->phi_.initialize(parent, nullptr); } +// TimeoutFilter +optional TimeoutFilter::new_value(float value) { + this->set_timeout("timeout", this->time_period_, [this]() { this->output(NAN); }); + this->output(value); + + return {}; +} + +TimeoutFilter::TimeoutFilter(uint32_t time_period) : time_period_(time_period) {} +float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } + // DebounceFilter optional DebounceFilter::new_value(float value) { this->set_timeout("debounce", this->time_period_, [this, value]() { this->output(value); }); diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 05934a26e8..296990f34f 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -313,6 +313,18 @@ class ThrottleFilter : public Filter { uint32_t min_time_between_inputs_; }; +class TimeoutFilter : public Filter, public Component { + public: + explicit TimeoutFilter(uint32_t time_period); + + optional new_value(float value) override; + + float get_setup_priority() const override; + + protected: + uint32_t time_period_; +}; + class DebounceFilter : public Filter, public Component { public: explicit DebounceFilter(uint32_t time_period); diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.cpp b/esphome/components/sigma_delta_output/sigma_delta_output.cpp new file mode 100644 index 0000000000..d386f8db1a --- /dev/null +++ b/esphome/components/sigma_delta_output/sigma_delta_output.cpp @@ -0,0 +1,57 @@ +#include "sigma_delta_output.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sigma_delta_output { + +static const char *const TAG = "output.sigma_delta"; + +void SigmaDeltaOutput::setup() { + if (this->pin_) + this->pin_->setup(); +} + +void SigmaDeltaOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Sigma Delta Output:"); + LOG_PIN(" Pin: ", this->pin_); + if (this->state_change_trigger_) { + ESP_LOGCONFIG(TAG, " State change automation configured"); + } + if (this->turn_on_trigger_) { + ESP_LOGCONFIG(TAG, " Turn on automation configured"); + } + if (this->turn_off_trigger_) { + ESP_LOGCONFIG(TAG, " Turn off automation configured"); + } + LOG_UPDATE_INTERVAL(this); + LOG_FLOAT_OUTPUT(this); +} + +void SigmaDeltaOutput::update() { + this->accum_ += this->state_; + const bool next_value = this->accum_ > 0; + + if (next_value) { + this->accum_ -= 1.; + } + + if (next_value != this->value_) { + this->value_ = next_value; + if (this->pin_) { + this->pin_->digital_write(next_value); + } + + if (this->state_change_trigger_) { + this->state_change_trigger_->trigger(next_value); + } + + if (next_value && this->turn_on_trigger_) { + this->turn_on_trigger_->trigger(); + } else if (!next_value && this->turn_off_trigger_) { + this->turn_off_trigger_->trigger(); + } + } +} + +} // namespace sigma_delta_output +} // namespace esphome diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.h b/esphome/components/sigma_delta_output/sigma_delta_output.h index 5a5acd2dfb..8fd1e1f761 100644 --- a/esphome/components/sigma_delta_output/sigma_delta_output.h +++ b/esphome/components/sigma_delta_output/sigma_delta_output.h @@ -1,9 +1,12 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" namespace esphome { namespace sigma_delta_output { + class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { public: Trigger<> *get_turn_on_trigger() { @@ -25,31 +28,9 @@ class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { void set_pin(GPIOPin *pin) { this->pin_ = pin; }; void write_state(float state) override { this->state_ = state; } - void update() override { - this->accum_ += this->state_; - const bool next_value = this->accum_ > 0; - - if (next_value) { - this->accum_ -= 1.; - } - - if (next_value != this->value_) { - this->value_ = next_value; - if (this->pin_) { - this->pin_->digital_write(next_value); - } - - if (this->state_change_trigger_) { - this->state_change_trigger_->trigger(next_value); - } - - if (next_value && this->turn_on_trigger_) { - this->turn_on_trigger_->trigger(); - } else if (!next_value && this->turn_off_trigger_) { - this->turn_off_trigger_->trigger(); - } - } - } + void setup() override; + void dump_config() override; + void update() override; protected: GPIOPin *pin_{nullptr}; diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index e0fc9efb42..1528a05734 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -16,6 +16,22 @@ CODEOWNERS = ["@esphome/core"] spi_ns = cg.esphome_ns.namespace("spi") SPIComponent = spi_ns.class_("SPIComponent", cg.Component) SPIDevice = spi_ns.class_("SPIDevice") +SPIDataRate = spi_ns.enum("SPIDataRate") + +SPI_DATA_RATE_OPTIONS = { + 80e6: SPIDataRate.DATA_RATE_80MHZ, + 40e6: SPIDataRate.DATA_RATE_40MHZ, + 20e6: SPIDataRate.DATA_RATE_20MHZ, + 10e6: SPIDataRate.DATA_RATE_10MHZ, + 5e6: SPIDataRate.DATA_RATE_5MHZ, + 2e6: SPIDataRate.DATA_RATE_2MHZ, + 1e6: SPIDataRate.DATA_RATE_1MHZ, + 2e5: SPIDataRate.DATA_RATE_200KHZ, + 75e3: SPIDataRate.DATA_RATE_75KHZ, + 1e3: SPIDataRate.DATA_RATE_1KHZ, +} +SPI_DATA_RATE_SCHEMA = cv.All(cv.frequency, cv.enum(SPI_DATA_RATE_OPTIONS)) + MULTI_CONF = True CONF_FORCE_SW = "force_sw" diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index bacdad723b..f19518caae 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -67,6 +67,7 @@ enum SPIDataRate : uint32_t { DATA_RATE_10MHZ = 10000000, DATA_RATE_20MHZ = 20000000, DATA_RATE_40MHZ = 40000000, + DATA_RATE_80MHZ = 80000000, }; class SPIComponent : public Component { diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 095884997c..8afafcb5ce 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -954,10 +954,18 @@ void Sprinkler::pause() { } void Sprinkler::resume() { + if (this->standby()) { + ESP_LOGD(TAG, "resume called but standby is enabled; no action taken"); + return; + } + if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { - ESP_LOGD(TAG, "Resuming valve %u with %u seconds remaining", this->paused_valve_.value_or(0), - this->resume_duration_.value_or(0)); - this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); + // Resume only if valve has not been completed yet + if (!this->valve_cycle_complete_(this->paused_valve_.value())) { + ESP_LOGD(TAG, "Resuming valve %u with %u seconds remaining", this->paused_valve_.value_or(0), + this->resume_duration_.value_or(0)); + this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); + } this->reset_resume(); } else { ESP_LOGD(TAG, "No valve to resume!"); diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 48143b9e1a..f4abd845c8 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -96,6 +96,6 @@ async def setup_ssd1306(var, config): cg.add(var.init_invert(config[CONF_INVERT])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1322_base/__init__.py b/esphome/components/ssd1322_base/__init__.py index 434caf4e35..97fb0d2a74 100644 --- a/esphome/components/ssd1322_base/__init__.py +++ b/esphome/components/ssd1322_base/__init__.py @@ -46,6 +46,6 @@ async def setup_ssd1322(var, config): cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 68be287d2a..1a6f7fb519 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -50,6 +50,6 @@ async def setup_ssd1325(var, config): cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1327_base/__init__.py b/esphome/components/ssd1327_base/__init__.py index eada66a6e3..af2eb3489d 100644 --- a/esphome/components/ssd1327_base/__init__.py +++ b/esphome/components/ssd1327_base/__init__.py @@ -37,6 +37,6 @@ async def setup_ssd1327(var, config): cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1331_base/__init__.py b/esphome/components/ssd1331_base/__init__.py index 067f55a252..169c0eed1a 100644 --- a/esphome/components/ssd1331_base/__init__.py +++ b/esphome/components/ssd1331_base/__init__.py @@ -28,6 +28,6 @@ async def setup_ssd1331(var, config): cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py index 555d6c5e2e..2988dd4bf3 100644 --- a/esphome/components/ssd1351_base/__init__.py +++ b/esphome/components/ssd1351_base/__init__.py @@ -38,6 +38,6 @@ async def setup_ssd1351(var, config): cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index ae31f604a5..652d31662d 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -77,7 +77,7 @@ async def setup_st7735(var, config): cg.add(var.set_reset_pin(reset)) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index a81101f2d1..16c1e790bd 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -121,7 +121,7 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index fce11f63d6..5ce8894a8a 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -11,7 +11,7 @@ void TemplateBinarySensor::setup() { return; if (this->f_ != nullptr) { - this->publish_initial_state(*this->f_()); + this->publish_initial_state(this->f_().value_or(false)); } else { this->publish_initial_state(false); } diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index e002c4e3d8..a221cbaa60 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -43,7 +43,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation( single=True ), - cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, + cv.Optional(CONF_RESTORE_STATE): cv.invalid( + "The restore_state option has been removed in 2023.7.0. Use the restore_mode option instead" + ), } ) .extend(cv.COMPONENT_SCHEMA), @@ -70,7 +72,6 @@ async def to_code(config): ) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) - cg.add(var.set_restore_state(config[CONF_RESTORE_STATE])) @automation.register_action( diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5db346b99f..b2a221669e 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -40,9 +40,6 @@ float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWA Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void TemplateSwitch::setup() { - if (!this->restore_state_) - return; - optional initial_state = this->get_initial_state_with_restore_mode(); if (initial_state.has_value()) { @@ -57,10 +54,8 @@ void TemplateSwitch::setup() { } void TemplateSwitch::dump_config() { LOG_SWITCH("", "Template Switch", this); - ESP_LOGCONFIG(TAG, " Restore State: %s", YESNO(this->restore_state_)); ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); } -void TemplateSwitch::set_restore_state(bool restore_state) { this->restore_state_ = restore_state; } void TemplateSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index ef9b567451..bfe9ac25d6 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -15,7 +15,6 @@ class TemplateSwitch : public switch_::Switch, public Component { void dump_config() override; void set_state_lambda(std::function()> &&f); - void set_restore_state(bool restore_state); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -35,7 +34,6 @@ class TemplateSwitch : public switch_::Switch, public Component { Trigger<> *turn_on_trigger_; Trigger<> *turn_off_trigger_; Trigger<> *prev_trigger_{nullptr}; - bool restore_state_{false}; }; } // namespace template_ diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 434c6e65f3..8d7630bd1d 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -300,6 +300,7 @@ uint8_t TM1637Display::read_byte_() { uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { // ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); uint8_t pos = start_pos; + bool use_dot = false; for (; *str != '\0'; str++) { uint8_t data = TM1637_UNKNOWN_CHAR; if (*str >= ' ' && *str <= '~') @@ -312,14 +313,14 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { // XABCDEFG, but TM1637 is // XGFEDCBA if (this->inverted_) { // XABCDEFG > XGCBAFED - data = ((data & 0x80) ? 0x80 : 0) | // no move X - ((data & 0x40) ? 0x8 : 0) | // A - ((data & 0x20) ? 0x10 : 0) | // B - ((data & 0x10) ? 0x20 : 0) | // C - ((data & 0x8) ? 0x1 : 0) | // D - ((data & 0x4) ? 0x2 : 0) | // E - ((data & 0x2) ? 0x4 : 0) | // F - ((data & 0x1) ? 0x40 : 0); // G + data = ((data & 0x80) || use_dot ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x8 : 0) | // A + ((data & 0x20) ? 0x10 : 0) | // B + ((data & 0x10) ? 0x20 : 0) | // C + ((data & 0x8) ? 0x1 : 0) | // D + ((data & 0x4) ? 0x2 : 0) | // E + ((data & 0x2) ? 0x4 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G } else { // XABCDEFG > XGFEDCBA data = ((data & 0x80) ? 0x80 : 0) | // no move X @@ -331,18 +332,18 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { ((data & 0x2) ? 0x20 : 0) | // F ((data & 0x1) ? 0x40 : 0); // G } - if (*str == '.') { - if (pos != start_pos) - pos--; - this->buffer_[pos] |= 0b10000000; + use_dot = *str == '.'; + if (use_dot) { + if ((!this->inverted_) && (pos != start_pos)) { + this->buffer_[pos - 1] |= 0b10000000; + } } else { if (pos >= 6) { ESP_LOGE(TAG, "String is too long for the display!"); break; } - this->buffer_[pos] = data; + this->buffer_[pos++] = data; } - pos++; } return pos - start_pos; } diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index 583392cce3..66df78b62a 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -3,6 +3,11 @@ namespace esphome { namespace touchscreen { +void TouchscreenBinarySensor::setup() { + this->parent_->register_listener(this); + this->publish_initial_state(false); +} + void TouchscreenBinarySensor::touch(TouchPoint tp) { bool touched = (tp.x >= this->x_min_ && tp.x <= this->x_max_ && tp.y >= this->y_min_ && tp.y <= this->y_max_); diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index 701468aa1e..b56ae562b1 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -14,7 +14,7 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, public TouchListener, public Parented { public: - void setup() override { this->parent_->register_listener(this); } + void setup() override; /// Set the touch screen area where the button will detect the touch. void set_area(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 9b337fc02c..2eaa736171 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -7,6 +7,17 @@ namespace touchscreen { static const char *const TAG = "touchscreen"; +void Touchscreen::set_display(display::Display *display) { + this->display_ = display; + this->display_width_ = display->get_width(); + this->display_height_ = display->get_height(); + this->rotation_ = static_cast(display->get_rotation()); + + if (this->rotation_ == ROTATE_90_DEGREES || this->rotation_ == ROTATE_270_DEGREES) { + std::swap(this->display_width_, this->display_height_); + } +} + void Touchscreen::send_touch_(TouchPoint tp) { ESP_LOGV(TAG, "Touch (x=%d, y=%d)", tp.x, tp.y); this->touch_trigger_.trigger(tp); diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 0597759894..24b3191880 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -31,13 +31,8 @@ enum TouchRotation { class Touchscreen { public: - void set_display(display::DisplayBuffer *display) { - this->display_ = display; - this->display_width_ = display->get_width_internal(); - this->display_height_ = display->get_height_internal(); - this->rotation_ = static_cast(display->get_rotation()); - } - display::DisplayBuffer *get_display() const { return this->display_; } + void set_display(display::Display *display); + display::Display *get_display() const { return this->display_; } Trigger *get_touch_trigger() { return &this->touch_trigger_; } @@ -49,7 +44,7 @@ class Touchscreen { uint16_t display_width_; uint16_t display_height_; - display::DisplayBuffer *display_; + display::Display *display_; TouchRotation rotation_; Trigger touch_trigger_; std::vector touch_listeners_; diff --git a/esphome/components/tt21100/__init__.py b/esphome/components/tt21100/__init__.py new file mode 100644 index 0000000000..a309d34beb --- /dev/null +++ b/esphome/components/tt21100/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@kroimon"] + +tt21100_ns = cg.esphome_ns.namespace("tt21100") diff --git a/esphome/components/tt21100/binary_sensor/__init__.py b/esphome/components/tt21100/binary_sensor/__init__.py new file mode 100644 index 0000000000..d5423a01b2 --- /dev/null +++ b/esphome/components/tt21100/binary_sensor/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_INDEX + +from .. import tt21100_ns +from ..touchscreen import TT21100Touchscreen, TT21100ButtonListener + +CONF_TT21100_ID = "tt21100_id" + +TT21100Button = tt21100_ns.class_( + "TT21100Button", + binary_sensor.BinarySensor, + cg.Component, + TT21100ButtonListener, + cg.Parented.template(TT21100Touchscreen), +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TT21100Button).extend( + { + cv.GenerateID(CONF_TT21100_ID): cv.use_id(TT21100Touchscreen), + cv.Required(CONF_INDEX): cv.int_range(min=0, max=3), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_TT21100_ID]) + cg.add(var.set_index(config[CONF_INDEX])) diff --git a/esphome/components/tt21100/binary_sensor/tt21100_button.cpp b/esphome/components/tt21100/binary_sensor/tt21100_button.cpp new file mode 100644 index 0000000000..2d5ac22a83 --- /dev/null +++ b/esphome/components/tt21100/binary_sensor/tt21100_button.cpp @@ -0,0 +1,27 @@ +#include "tt21100_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tt21100 { + +static const char *const TAG = "tt21100.binary_sensor"; + +void TT21100Button::setup() { + this->parent_->register_button_listener(this); + this->publish_initial_state(false); +} + +void TT21100Button::dump_config() { + LOG_BINARY_SENSOR("", "TT21100 Button", this); + ESP_LOGCONFIG(TAG, " Index: %u", this->index_); +} + +void TT21100Button::update_button(uint8_t index, uint16_t state) { + if (index != this->index_) + return; + + this->publish_state(state > 0); +} + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tt21100/binary_sensor/tt21100_button.h b/esphome/components/tt21100/binary_sensor/tt21100_button.h new file mode 100644 index 0000000000..90b55bb75a --- /dev/null +++ b/esphome/components/tt21100/binary_sensor/tt21100_button.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/tt21100/touchscreen/tt21100.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tt21100 { + +class TT21100Button : public binary_sensor::BinarySensor, + public Component, + public TT21100ButtonListener, + public Parented { + public: + void setup() override; + void dump_config() override; + + void set_index(uint8_t index) { this->index_ = index; } + + void update_button(uint8_t index, uint16_t state) override; + + protected: + uint8_t index_; +}; + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tt21100/touchscreen/__init__.py b/esphome/components/tt21100/touchscreen/__init__.py new file mode 100644 index 0000000000..d96d389e69 --- /dev/null +++ b/esphome/components/tt21100/touchscreen/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +from .. import tt21100_ns + +DEPENDENCIES = ["i2c"] + +TT21100Touchscreen = tt21100_ns.class_( + "TT21100Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) +TT21100ButtonListener = tt21100_ns.class_("TT21100ButtonListener") + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TT21100Touchscreen), + cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x24)) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await touchscreen.register_touchscreen(var, config) + + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) + + if CONF_RESET_PIN in config: + rts_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(rts_pin)) diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp new file mode 100644 index 0000000000..28a8c2d754 --- /dev/null +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -0,0 +1,175 @@ +#include "tt21100.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tt21100 { + +static const char *const TAG = "tt21100"; + +static const uint8_t MAX_BUTTONS = 4; +static const uint8_t MAX_TOUCH_POINTS = 5; +static const uint8_t MAX_DATA_LEN = (7 + MAX_TOUCH_POINTS * 10); // 7 Header + (Points * 10 data bytes) + +struct TT21100ButtonReport { + uint16_t length; // Always 14 (0x000E) + uint8_t report_id; // Always 0x03 + uint16_t timestamp; // Number in units of 100 us + uint8_t btn_value; // Only use bit 0..3 + uint16_t btn_signal[MAX_BUTTONS]; +} __attribute__((packed)); + +struct TT21100TouchRecord { + uint8_t : 5; + uint8_t touch_type : 3; + uint8_t tip : 1; + uint8_t event_id : 2; + uint8_t touch_id : 5; + uint16_t x; + uint16_t y; + uint8_t pressure; + uint16_t major_axis_length; + uint8_t orientation; +} __attribute__((packed)); + +struct TT21100TouchReport { + uint16_t length; + uint8_t report_id; + uint16_t timestamp; + uint8_t : 2; + uint8_t large_object : 1; + uint8_t record_num : 5; + uint8_t report_counter : 2; + uint8_t : 3; + uint8_t noise_effect : 3; + TT21100TouchRecord touch_record[MAX_TOUCH_POINTS]; +} __attribute__((packed)); + +void TT21100TouchscreenStore::gpio_intr(TT21100TouchscreenStore *store) { store->touch = true; } + +float TT21100Touchscreen::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } + +void TT21100Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up TT21100 Touchscreen..."); + + // Register interrupt pin + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + this->store_.pin = this->interrupt_pin_->to_isr(); + this->interrupt_pin_->attach_interrupt(TT21100TouchscreenStore::gpio_intr, &this->store_, + gpio::INTERRUPT_FALLING_EDGE); + + // Perform reset if necessary + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_(); + } + + // Update display dimensions if they were updated during display setup + this->display_width_ = this->display_->get_width(); + this->display_height_ = this->display_->get_height(); + this->rotation_ = static_cast(this->display_->get_rotation()); + + // Trigger initial read to activate the interrupt + this->store_.touch = true; +} + +void TT21100Touchscreen::loop() { + if (!this->store_.touch) + return; + this->store_.touch = false; + + // Read report length + uint16_t data_len; + this->read((uint8_t *) &data_len, sizeof(data_len)); + + // Read report data + uint8_t data[MAX_DATA_LEN]; + if (data_len > 0 && data_len < sizeof(data)) { + this->read(data, data_len); + + if (data_len == 14) { + // Button event + auto *report = (TT21100ButtonReport *) data; + + ESP_LOGV(TAG, "Button report: Len=%d, ID=%d, Time=%5u, Value=[%u], Signal=[%04X][%04X][%04X][%04X]", + report->length, report->report_id, report->timestamp, report->btn_value, report->btn_signal[0], + report->btn_signal[1], report->btn_signal[2], report->btn_signal[3]); + + for (uint8_t i = 0; i < 4; i++) { + for (auto *listener : this->button_listeners_) + listener->update_button(i, report->btn_signal[i]); + } + + } else if (data_len >= 7) { + // Touch point event + auto *report = (TT21100TouchReport *) data; + + ESP_LOGV(TAG, + "Touch report: Len=%d, ID=%d, Time=%5u, LargeObject=%u, RecordNum=%u, RecordCounter=%u, NoiseEffect=%u", + report->length, report->report_id, report->timestamp, report->large_object, report->record_num, + report->report_counter, report->noise_effect); + + uint8_t touch_count = (data_len - (sizeof(*report) - sizeof(report->touch_record))) / sizeof(TT21100TouchRecord); + + if (touch_count == 0) { + for (auto *listener : this->touch_listeners_) + listener->release(); + return; + } + + for (int i = 0; i < touch_count; i++) { + auto *touch = &report->touch_record[i]; + + ESP_LOGV(TAG, + "Touch %d: Type=%u, Tip=%u, EventId=%u, TouchId=%u, X=%u, Y=%u, Pressure=%u, MajorAxisLen=%u, " + "Orientation=%u", + i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y, + touch->pressure, touch->major_axis_length, touch->orientation); + + TouchPoint tp; + switch (this->rotation_) { + case ROTATE_0_DEGREES: + // Origin is top right, so mirror X by default + tp.x = this->display_width_ - touch->x; + tp.y = touch->y; + break; + case ROTATE_90_DEGREES: + tp.x = touch->y; + tp.y = touch->x; + break; + case ROTATE_180_DEGREES: + tp.x = touch->x; + tp.y = this->display_height_ - touch->y; + break; + case ROTATE_270_DEGREES: + tp.x = this->display_height_ - touch->y; + tp.y = this->display_width_ - touch->x; + break; + } + tp.id = touch->tip; + tp.state = touch->pressure; + + this->defer([this, tp]() { this->send_touch_(tp); }); + } + } + } +} + +void TT21100Touchscreen::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + } +} + +void TT21100Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "TT21100 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tt21100/touchscreen/tt21100.h b/esphome/components/tt21100/touchscreen/tt21100.h new file mode 100644 index 0000000000..306360975f --- /dev/null +++ b/esphome/components/tt21100/touchscreen/tt21100.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tt21100 { + +using namespace touchscreen; + +struct TT21100TouchscreenStore { + volatile bool touch; + ISRInternalGPIOPin pin; + + static void gpio_intr(TT21100TouchscreenStore *store); +}; + +class TT21100ButtonListener { + public: + virtual void update_button(uint8_t index, uint16_t state) = 0; +}; + +class TT21100Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + void register_button_listener(TT21100ButtonListener *listener) { this->button_listeners_.push_back(listener); } + + protected: + void reset_(); + + TT21100TouchscreenStore store_; + + InternalGPIOPin *interrupt_pin_; + GPIOPin *reset_pin_{nullptr}; + + std::vector button_listeners_; +}; + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 7b7a974de2..66931767b2 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -168,7 +168,7 @@ void TuyaLight::write_state(light::LightState *state) { if (brightness > 0.0f || !color_interlock_) { if (this->color_temperature_id_.has_value()) { - uint32_t color_temp_int = static_cast(color_temperature * this->color_temperature_max_value_); + uint32_t color_temp_int = static_cast(roundf(color_temperature * this->color_temperature_max_value_)); if (this->color_temperature_invert_) { color_temp_int = this->color_temperature_max_value_ - color_temp_int; } diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index ed60a9f880..aea59d9d8b 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -246,6 +246,7 @@ def final_validate_device_schema( baud_rate: Optional[int] = None, require_tx: bool = False, require_rx: bool = False, + data_bits: Optional[int] = None, parity: Optional[str] = None, stop_bits: Optional[int] = None, ): @@ -268,6 +269,13 @@ def final_validate_device_schema( return validator + def validate_data_bits(value): + if value != data_bits: + raise cv.Invalid( + f"Component {name} requires {data_bits} data bits for the uart bus" + ) + return value + def validate_parity(value): if value != parity: raise cv.Invalid( @@ -278,7 +286,7 @@ def final_validate_device_schema( def validate_stop_bits(value): if value != stop_bits: raise cv.Invalid( - f"Component {name} requires stop bits {stop_bits} for the uart bus" + f"Component {name} requires {stop_bits} stop bits for the uart bus" ) return value @@ -304,6 +312,8 @@ def final_validate_device_schema( ] = validate_pin(CONF_RX_PIN, device) if baud_rate is not None: hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate + if data_bits is not None: + hub_schema[cv.Required(CONF_DATA_BITS)] = validate_data_bits if parity is not None: hub_schema[cv.Required(CONF_PARITY)] = validate_parity if stop_bits is not None: diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 44d640ff39..217ddb6354 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -130,7 +130,7 @@ void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { void VoiceAssistant::request_start(bool continuous) { ESP_LOGD(TAG, "Requesting start..."); - if (!api::global_api_server->start_voice_assistant(this->conversation_id_)) { + if (!api::global_api_server->start_voice_assistant(this->conversation_id_, this->silence_detection_)) { ESP_LOGW(TAG, "Could not request start."); this->error_trigger_->trigger("not-connected", "Could not request start."); this->continuous_ = false; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index b103584509..e67baaee65 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -25,10 +25,9 @@ namespace voice_assistant { // Version 1: Initial version // Version 2: Adds raw speaker support -// Version 3: Adds continuous support +// Version 3: Unused/skip static const uint32_t INITIAL_VERSION = 1; static const uint32_t SPEAKER_SUPPORT = 2; -static const uint32_t SILENCE_DETECTION_SUPPORT = 3; class VoiceAssistant : public Component { public: @@ -48,9 +47,6 @@ class VoiceAssistant : public Component { uint32_t get_version() const { #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { - if (this->silence_detection_) { - return SILENCE_DETECTION_SUPPORT; - } return SPEAKER_SUPPORT; } #endif diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index d0276f119a..6113fe943c 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -63,6 +63,7 @@ WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( WaveshareEPaper2P13InDKE = waveshare_epaper_ns.class_( "WaveshareEPaper2P13InDKE", WaveshareEPaper ) +GDEW0154M09 = waveshare_epaper_ns.class_("GDEW0154M09", WaveshareEPaper) WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel") WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeBModel") @@ -91,6 +92,7 @@ MODELS = { "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt), "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), + "1.54in-m5coreink-m09": ("c", GDEW0154M09), } @@ -151,7 +153,7 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) if CONF_RESET_PIN in config: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 42f5bc54e3..d64a5500dd 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -763,6 +763,146 @@ void GDEY029T94::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// Good Display 1.54in black/white/grey GDEW0154M09 +// As used in M5Stack Core Ink +// Datasheet: +// - https://v4.cecdn.yun300.cn/100001_1909185148/GDEW0154M09-200709.pdf +// - https://github.com/m5stack/M5Core-Ink +// Reference code from GoodDisplay: +// - https://github.com/GoodDisplay/E-paper-Display-Library-of-GoodDisplay/ +// -> /Monochrome_E-paper-Display/1.54inch_JD79653_GDEW0154M09_200x200/ESP32-Arduino%20IDE/GDEW0154M09_Arduino.ino +// M5Stack Core Ink spec: +// - https://docs.m5stack.com/en/core/coreink +// ======================================================== + +void GDEW0154M09::initialize() { + this->init_internal_(); + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->lastbuff_ = allocator.allocate(this->get_buffer_length_()); + if (this->lastbuff_ != nullptr) { + memset(this->lastbuff_, 0xff, sizeof(uint8_t) * this->get_buffer_length_()); + } + this->clear_(); +} + +void GDEW0154M09::reset_() { + // RST is inverse from other einks in this project + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + } +} + +void GDEW0154M09::init_internal_() { + this->reset_(); + + // clang-format off + // 200x200 resolution: 11 + // LUT from OTP: 0 + // B/W mode (doesn't work): 1 + // scan-up: 1 + // shift-right: 1 + // booster ON: 1 + // no soft reset: 1 + const uint8_t panel_setting_1 = 0b11011111; + + // VCOM status off 0 + // Temp sensing default 1 + // VGL Power Off Floating 1 + // NORG expect refresh 1 + // VCOM Off on displ off 0 + const uint8_t panel_setting_2 = 0b01110; + + const uint8_t wf_t0154_cz_b3_list[] = { + 11, // 11 commands in list + CMD_PSR_PANEL_SETTING, 2, panel_setting_1, panel_setting_2, + CMD_UNDOCUMENTED_0x4D, 1, 0x55, + CMD_UNDOCUMENTED_0xAA, 1, 0x0f, + CMD_UNDOCUMENTED_0xE9, 1, 0x02, + CMD_UNDOCUMENTED_0xB6, 1, 0x11, + CMD_UNDOCUMENTED_0xF3, 1, 0x0a, + CMD_TRES_RESOLUTION_SETTING, 3, 0xc8, 0x00, 0xc8, + CMD_TCON_TCONSETTING, 1, 0x00, + CMD_CDI_VCOM_DATA_INTERVAL, 1, 0xd7, + CMD_PWS_POWER_SAVING, 1, 0x00, + CMD_PON_POWER_ON, 0 + }; + // clang-format on + + this->write_init_list_(wf_t0154_cz_b3_list); + delay(100); // NOLINT + this->wait_until_idle_(); +} + +void GDEW0154M09::write_init_list_(const uint8_t *list) { + uint8_t list_limit = list[0]; + uint8_t *start_ptr = ((uint8_t *) list + 1); + for (uint8_t i = 0; i < list_limit; i++) { + this->command(*(start_ptr + 0)); + for (uint8_t dnum = 0; dnum < *(start_ptr + 1); dnum++) { + this->data(*(start_ptr + 2 + dnum)); + } + start_ptr += (*(start_ptr + 1) + 2); + } +} + +void GDEW0154M09::clear_() { + uint32_t pixsize = this->get_buffer_length_(); + for (uint8_t j = 0; j < 2; j++) { + this->command(CMD_DTM1_DATA_START_TRANS); + for (int count = 0; count < pixsize; count++) { + this->data(0x00); + } + this->command(CMD_DTM2_DATA_START_TRANS2); + for (int count = 0; count < pixsize; count++) { + this->data(0xff); + } + this->command(CMD_DISPLAY_REFRESH); + delay(10); + this->wait_until_idle_(); + } +} + +void HOT GDEW0154M09::display() { + this->init_internal_(); + // "Mode 0 display" for now + this->command(CMD_DTM1_DATA_START_TRANS); + for (int i = 0; i < this->get_buffer_length_(); i++) { + this->data(0xff); + } + this->command(CMD_DTM2_DATA_START_TRANS2); // write 'new' data to SRAM + for (int i = 0; i < this->get_buffer_length_(); i++) { + this->data(this->buffer_[i]); + } + this->command(CMD_DISPLAY_REFRESH); + delay(10); + this->wait_until_idle_(); + this->deep_sleep(); +} + +void GDEW0154M09::deep_sleep() { + // COMMAND DEEP SLEEP + this->command(CMD_POF_POWER_OFF); + this->wait_until_idle_(); + delay(1000); // NOLINT + this->command(CMD_DSLP_DEEP_SLEEP); + this->data(DATA_DSLP_DEEP_SLEEP); +} + +int GDEW0154M09::get_width_internal() { return 200; } +int GDEW0154M09::get_height_internal() { return 200; } +void GDEW0154M09::dump_config() { + LOG_DISPLAY("", "M5Stack CoreInk E-Paper (Good Display)", this); + ESP_LOGCONFIG(TAG, " Model: 1.54in Greyscale GDEW0154M09"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + static const uint8_t LUT_VCOM_DC_4_2[] = { 0x00, 0x17, 0x00, 0x00, 0x00, 0x02, 0x00, 0x17, 0x17, 0x00, 0x00, 0x02, 0x00, 0x0A, 0x01, 0x00, 0x00, 0x01, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 1cb46bdb9d..a84a1d4541 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -170,6 +170,46 @@ class GDEY029T94 : public WaveshareEPaper { int get_height_internal() override; }; +class GDEW0154M09 : public WaveshareEPaper { + public: + void initialize() override; + void display() override; + void dump_config() override; + void deep_sleep() override; + + protected: + int get_width_internal() override; + int get_height_internal() override; + + private: + static const uint8_t CMD_DTM1_DATA_START_TRANS = 0x10; + static const uint8_t CMD_DTM2_DATA_START_TRANS2 = 0x13; + static const uint8_t CMD_DISPLAY_REFRESH = 0x12; + static const uint8_t CMD_AUTO_SEQ = 0x17; + static const uint8_t DATA_AUTO_PON_DSR_POF_DSLP = 0xA7; + static const uint8_t CMD_PSR_PANEL_SETTING = 0x00; + static const uint8_t CMD_UNDOCUMENTED_0x4D = 0x4D; // NOLINT + static const uint8_t CMD_UNDOCUMENTED_0xAA = 0xaa; // NOLINT + static const uint8_t CMD_UNDOCUMENTED_0xE9 = 0xe9; // NOLINT + static const uint8_t CMD_UNDOCUMENTED_0xB6 = 0xb6; // NOLINT + static const uint8_t CMD_UNDOCUMENTED_0xF3 = 0xf3; // NOLINT + static const uint8_t CMD_TRES_RESOLUTION_SETTING = 0x61; + static const uint8_t CMD_TCON_TCONSETTING = 0x60; + static const uint8_t CMD_CDI_VCOM_DATA_INTERVAL = 0x50; + static const uint8_t CMD_POF_POWER_OFF = 0x02; + static const uint8_t CMD_DSLP_DEEP_SLEEP = 0x07; + static const uint8_t DATA_DSLP_DEEP_SLEEP = 0xA5; + static const uint8_t CMD_PWS_POWER_SAVING = 0xe3; + static const uint8_t CMD_PON_POWER_ON = 0x04; + static const uint8_t CMD_PTL_PARTIAL_WINDOW = 0x90; + + uint8_t *lastbuff_ = nullptr; + void reset_(); + void clear_(); + void write_init_list_(const uint8_t *list); + void init_internal_(); +}; + class WaveshareEPaper2P9InB : public WaveshareEPaper { public: void initialize() override; diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 25c0254f90..ab54ae8582 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -47,6 +47,12 @@ def validate_local(config): return config +def validate_ota(config): + if CORE.using_esp_idf and config[CONF_OTA]: + raise cv.Invalid("Enabling 'ota' is not supported for IDF framework yet") + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -71,15 +77,17 @@ CONFIG_SCHEMA = cv.All( web_server_base.WebServerBase ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, - cv.Optional(CONF_OTA, default=True): cv.boolean, + cv.SplitDefault( + CONF_OTA, esp8266=True, esp32_arduino=True, esp32_idf=False + ): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_arduino, cv.only_on(["esp32", "esp8266"]), default_url, validate_local, + validate_ota, ) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index ce7b4be7f3..016dd37dd9 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "list_entities.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -103,5 +101,3 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 8ddca15edf..1569c8ac57 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -1,7 +1,5 @@ #pragma once -#ifdef USE_ARDUINO - #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" #include "esphome/core/defines.h" @@ -59,5 +57,3 @@ class ListEntitiesIterator : public ComponentIterator { } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index eb1858a09c..01057fead6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "web_server.h" #include "esphome/components/json/json_util.h" @@ -9,7 +7,9 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" +#ifdef USE_ARDUINO #include "StreamString.h" +#endif #include @@ -181,7 +181,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("")); #endif if (strlen(this->css_url_) > 0) { - stream->print(F("print(F(R"(print(this->css_url_); stream->print(F("\">")); } @@ -381,7 +381,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { return json::build_json([obj, value, start_config](JsonObject root) { std::string state; - if (isnan(value)) { + if (std::isnan(value)) { state = "NA"; } else { state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); @@ -524,11 +524,8 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); - if (request->hasParam("speed")) { - String speed = request->getParam("speed")->value(); - } if (request->hasParam("speed_level")) { - String speed_level = request->getParam("speed_level")->value(); + auto speed_level = request->getParam("speed_level")->value(); 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()); @@ -537,7 +534,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc call.set_speed(*val); } if (request->hasParam("oscillation")) { - String speed = request->getParam("oscillation")->value(); + auto speed = request->getParam("oscillation")->value(); auto val = parse_on_off(speed.c_str()); switch (val) { case PARSE_ON: @@ -585,29 +582,54 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); - if (request->hasParam("brightness")) - call.set_brightness(request->getParam("brightness")->value().toFloat() / 255.0f); - if (request->hasParam("r")) - call.set_red(request->getParam("r")->value().toFloat() / 255.0f); - if (request->hasParam("g")) - call.set_green(request->getParam("g")->value().toFloat() / 255.0f); - if (request->hasParam("b")) - call.set_blue(request->getParam("b")->value().toFloat() / 255.0f); - if (request->hasParam("white_value")) - call.set_white(request->getParam("white_value")->value().toFloat() / 255.0f); - if (request->hasParam("color_temp")) - call.set_color_temperature(request->getParam("color_temp")->value().toFloat()); - + if (request->hasParam("brightness")) { + auto brightness = parse_number(request->getParam("brightness")->value().c_str()); + if (brightness.has_value()) { + call.set_brightness(*brightness / 255.0f); + } + } + if (request->hasParam("r")) { + auto r = parse_number(request->getParam("r")->value().c_str()); + if (r.has_value()) { + call.set_red(*r / 255.0f); + } + } + if (request->hasParam("g")) { + auto g = parse_number(request->getParam("g")->value().c_str()); + if (g.has_value()) { + call.set_green(*g / 255.0f); + } + } + if (request->hasParam("b")) { + auto b = parse_number(request->getParam("b")->value().c_str()); + if (b.has_value()) { + call.set_blue(*b / 255.0f); + } + } + if (request->hasParam("white_value")) { + auto white_value = parse_number(request->getParam("white_value")->value().c_str()); + if (white_value.has_value()) { + call.set_white(*white_value / 255.0f); + } + } + if (request->hasParam("color_temp")) { + auto color_temp = parse_number(request->getParam("color_temp")->value().c_str()); + if (color_temp.has_value()) { + call.set_color_temperature(*color_temp); + } + } if (request->hasParam("flash")) { - float length_s = request->getParam("flash")->value().toFloat(); - call.set_flash_length(static_cast(length_s * 1000)); + auto flash = parse_number(request->getParam("flash")->value().c_str()); + if (flash.has_value()) { + call.set_flash_length(*flash * 1000); + } } - if (request->hasParam("transition")) { - float length_s = request->getParam("transition")->value().toFloat(); - call.set_transition_length(static_cast(length_s * 1000)); + auto transition = parse_number(request->getParam("transition")->value().c_str()); + if (transition.has_value()) { + call.set_transition_length(*transition * 1000); + } } - if (request->hasParam("effect")) { const char *effect = request->getParam("effect")->value().c_str(); call.set_effect(effect); @@ -618,8 +640,10 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } else if (match.method == "turn_off") { auto call = obj->turn_off(); if (request->hasParam("transition")) { - auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000; - call.set_transition_length(length); + auto transition = parse_number(request->getParam("transition")->value().c_str()); + if (transition.has_value()) { + call.set_transition_length(*transition * 1000); + } } this->schedule_([call]() mutable { call.perform(); }); request->send(200); @@ -681,10 +705,18 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa return; } - if (request->hasParam("position")) - call.set_position(request->getParam("position")->value().toFloat()); - if (request->hasParam("tilt")) - call.set_tilt(request->getParam("tilt")->value().toFloat()); + if (request->hasParam("position")) { + auto position = parse_number(request->getParam("position")->value().c_str()); + if (position.has_value()) { + call.set_position(*position); + } + } + if (request->hasParam("tilt")) { + auto tilt = parse_number(request->getParam("tilt")->value().c_str()); + if (tilt.has_value()) { + call.set_tilt(*tilt); + } + } this->schedule_([call]() mutable { call.perform(); }); request->send(200); @@ -725,10 +757,9 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); if (request->hasParam("value")) { - String value = request->getParam("value")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_value(*value_f); + auto value = parse_number(request->getParam("value")->value().c_str()); + if (value.has_value()) + call.set_value(*value); } this->schedule_([call]() mutable { call.perform(); }); @@ -747,7 +778,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["step"] = obj->traits.get_step(); root["mode"] = (int) obj->traits.get_mode(); } - if (isnan(value)) { + if (std::isnan(value)) { root["value"] = "\"NaN\""; root["state"] = "NA"; } else { @@ -784,7 +815,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); if (request->hasParam("option")) { - String option = request->getParam("option")->value(); + auto option = request->getParam("option")->value(); call.set_option(option.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) } @@ -834,29 +865,26 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url auto call = obj->make_call(); if (request->hasParam("mode")) { - String mode = request->getParam("mode")->value(); + auto mode = request->getParam("mode")->value(); call.set_mode(mode.c_str()); } if (request->hasParam("target_temperature_high")) { - String value = request->getParam("target_temperature_high")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_target_temperature_high(*value_f); + auto target_temperature_high = parse_number(request->getParam("target_temperature_high")->value().c_str()); + if (target_temperature_high.has_value()) + call.set_target_temperature_high(*target_temperature_high); } if (request->hasParam("target_temperature_low")) { - String value = request->getParam("target_temperature_low")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_target_temperature_low(*value_f); + auto target_temperature_low = parse_number(request->getParam("target_temperature_low")->value().c_str()); + if (target_temperature_low.has_value()) + call.set_target_temperature_low(*target_temperature_low); } if (request->hasParam("target_temperature")) { - String value = request->getParam("target_temperature")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_target_temperature(*value_f); + auto target_temperature = parse_number(request->getParam("target_temperature")->value().c_str()); + if (target_temperature.has_value()) + call.set_target_temperature(*target_temperature); } this->schedule_([call]() mutable { call.perform(); }); @@ -1231,5 +1259,3 @@ void WebServer::schedule_(std::function &&f) { } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 83ebba205f..788e30ccf2 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -1,7 +1,5 @@ #pragma once -#ifdef USE_ARDUINO - #include "list_entities.h" #include "esphome/components/web_server_base/web_server_base.h" @@ -291,5 +289,3 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 14fb033a56..87f23a990a 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -5,7 +5,15 @@ from esphome.core import coroutine_with_priority, CORE CODEOWNERS = ["@OttoWinter"] DEPENDENCIES = ["network"] -AUTO_LOAD = ["async_tcp"] + + +def AUTO_LOAD(): + if CORE.using_arduino: + return ["async_tcp"] + if CORE.using_esp_idf: + return ["web_server_idf"] + return [] + web_server_base_ns = cg.esphome_ns.namespace("web_server_base") WebServerBase = web_server_base_ns.class_("WebServerBase", cg.Component) @@ -23,9 +31,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CORE.is_esp32: - cg.add_library("WiFi", None) - 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.1.0") + if CORE.using_arduino: + if CORE.is_esp32: + cg.add_library("WiFi", None) + 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.1.0") diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 3c269b28b8..997ce0798a 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -1,16 +1,17 @@ -#ifdef USE_ARDUINO - #include "web_server_base.h" #include "esphome/core/log.h" #include "esphome/core/application.h" -#include +#include "esphome/core/helpers.h" +#ifdef USE_ARDUINO +#include #ifdef USE_ESP32 #include #endif #ifdef USE_ESP8266 #include #endif +#endif namespace esphome { namespace web_server_base { @@ -24,18 +25,22 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); } this->handlers_.push_back(handler); - if (this->server_ != nullptr) + if (this->server_ != nullptr) { this->server_->addHandler(handler); + } } void report_ota_error() { +#ifdef USE_ARDUINO StreamString ss; Update.printError(ss); ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str()); +#endif } void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { +#ifdef USE_ARDUINO bool success; if (index == 0) { ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str()); @@ -45,9 +50,10 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin // NOLINTNEXTLINE(readability-static-accessed-through-instance) success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); #endif -#ifdef USE_ESP32 - if (Update.isRunning()) +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + if (Update.isRunning()) { Update.abort(); + } success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH); #endif if (!success) { @@ -85,8 +91,10 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin report_ota_error(); } } +#endif } void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { +#ifdef USE_ARDUINO AsyncWebServerResponse *response; if (!Update.hasError()) { response = request->beginResponse(200, "text/plain", "Update Successful!"); @@ -98,10 +106,13 @@ void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { } response->addHeader("Connection", "close"); request->send(response); +#endif } void WebServerBase::add_ota_handler() { +#ifdef USE_ARDUINO this->add_handler(new OTARequestHandler(this)); // NOLINT +#endif } float WebServerBase::get_setup_priority() const { // Before WiFi (captive portal) @@ -110,5 +121,3 @@ float WebServerBase::get_setup_priority() const { } // namespace web_server_base } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index ae286b1e22..c312126472 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -1,14 +1,17 @@ #pragma once -#ifdef USE_ARDUINO - #include #include #include #include "esphome/core/component.h" +#ifdef USE_ARDUINO #include +#elif USE_ESP_IDF +#include "esphome/core/hal.h" +#include "esphome/components/web_server_idf/web_server_idf.h" +#endif namespace esphome { namespace web_server_base { @@ -141,5 +144,3 @@ class OTARequestHandler : public AsyncWebHandler { } // namespace web_server_base } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py new file mode 100644 index 0000000000..bd3db24bc6 --- /dev/null +++ b/esphome/components/web_server_idf/__init__.py @@ -0,0 +1,14 @@ +import esphome.config_validation as cv +from esphome.components.esp32 import add_idf_sdkconfig_option + +CODEOWNERS = ["@dentra"] + +CONFIG_SCHEMA = cv.All( + cv.Schema({}), + cv.only_with_esp_idf, +) + + +async def to_code(config): + # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server + add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp new file mode 100644 index 0000000000..444e682460 --- /dev/null +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -0,0 +1,374 @@ +#ifdef USE_ESP_IDF + +#include + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include "esp_tls_crypto.h" + +#include "web_server_idf.h" + +namespace esphome { +namespace web_server_idf { + +#ifndef HTTPD_409 +#define HTTPD_409 "409 Conflict" +#endif + +#define CRLF_STR "\r\n" +#define CRLF_LEN (sizeof(CRLF_STR) - 1) + +static const char *const TAG = "web_server_idf"; + +void AsyncWebServer::end() { + if (this->server_) { + httpd_stop(this->server_); + this->server_ = nullptr; + } +} + +void AsyncWebServer::begin() { + if (this->server_) { + this->end(); + } + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = this->port_; + config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; + if (httpd_start(&this->server_, &config) == ESP_OK) { + const httpd_uri_t handler_get = { + .uri = "", + .method = HTTP_GET, + .handler = AsyncWebServer::request_handler, + .user_ctx = this, + }; + httpd_register_uri_handler(this->server_, &handler_get); + + const httpd_uri_t handler_post = { + .uri = "", + .method = HTTP_POST, + .handler = AsyncWebServer::request_handler, + .user_ctx = this, + }; + httpd_register_uri_handler(this->server_, &handler_post); + } +} + +esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) { + ESP_LOGV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri); + AsyncWebServerRequest req(r); + auto *server = static_cast(r->user_ctx); + for (auto *handler : server->handlers_) { + if (handler->canHandle(&req)) { + // At now process only basic requests. + // OTA requires multipart request support and handleUpload for it + handler->handleRequest(&req); + return ESP_OK; + } + } + if (server->on_not_found_) { + server->on_not_found_(&req); + return ESP_OK; + } + return ESP_ERR_NOT_FOUND; +} + +AsyncWebServerRequest::~AsyncWebServerRequest() { + delete this->rsp_; + for (const auto &pair : this->params_) { + delete pair.second; // NOLINT(cppcoreguidelines-owning-memory) + } +} + +optional AsyncWebServerRequest::get_header(const char *name) const { + size_t buf_len = httpd_req_get_hdr_value_len(*this, name); + if (buf_len == 0) { + return {}; + } + auto buf = std::unique_ptr(new char[++buf_len]); + if (!buf) { + ESP_LOGE(TAG, "No enough memory for get header %s", name); + return {}; + } + if (httpd_req_get_hdr_value_str(*this, name, buf.get(), buf_len) != ESP_OK) { + return {}; + } + return {buf.get()}; +} + +std::string AsyncWebServerRequest::url() const { + auto *str = strchr(this->req_->uri, '?'); + if (str == nullptr) { + return this->req_->uri; + } + return std::string(this->req_->uri, str - this->req_->uri); +} + +std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } + +void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { + httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); +} + +void AsyncWebServerRequest::send(int code, const char *content_type, const char *content) { + this->init_response_(nullptr, code, content_type); + if (content) { + httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); + } else { + httpd_resp_send(*this, nullptr, 0); + } +} + +void AsyncWebServerRequest::redirect(const std::string &url) { + httpd_resp_set_status(*this, "302 Found"); + httpd_resp_set_hdr(*this, "Location", url.c_str()); + httpd_resp_send(*this, nullptr, 0); +} + +void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { + httpd_resp_set_status(*this, code == 200 ? HTTPD_200 + : code == 404 ? HTTPD_404 + : code == 409 ? HTTPD_409 + : to_string(code).c_str()); + + if (content_type && *content_type) { + httpd_resp_set_type(*this, content_type); + } + httpd_resp_set_hdr(*this, "Accept-Ranges", "none"); + + for (const auto &pair : DefaultHeaders::Instance().headers_) { + httpd_resp_set_hdr(*this, pair.first.c_str(), pair.second.c_str()); + } + + delete this->rsp_; + this->rsp_ = rsp; +} + +bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const { + if (username == nullptr || password == nullptr || *username == 0) { + return true; + } + auto auth = this->get_header("Authorization"); + if (!auth.has_value()) { + return false; + } + + auto *auth_str = auth.value().c_str(); + + const auto auth_prefix_len = sizeof("Basic ") - 1; + if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) { + ESP_LOGW(TAG, "Only Basic authorization supported yet"); + return false; + } + + std::string user_info; + user_info += username; + user_info += ':'; + user_info += password; + + size_t n = 0, out; + esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast(user_info.c_str()), user_info.size()); + + auto digest = std::unique_ptr(new char[n + 1]); + esp_crypto_base64_encode(reinterpret_cast(digest.get()), n, &out, + reinterpret_cast(user_info.c_str()), user_info.size()); + + return strncmp(digest.get(), auth_str + auth_prefix_len, auth.value().size() - auth_prefix_len) == 0; +} + +void AsyncWebServerRequest::requestAuthentication(const char *realm) const { + httpd_resp_set_hdr(*this, "Connection", "keep-alive"); + auto auth_val = str_sprintf("Basic realm=\"%s\"", realm ? realm : "Login Required"); + httpd_resp_set_hdr(*this, "WWW-Authenticate", auth_val.c_str()); + httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); +} + +static std::string url_decode(const std::string &in) { + std::string out; + out.reserve(in.size()); + for (std::size_t i = 0; i < in.size(); ++i) { + if (in[i] == '%') { + ++i; + if (i + 1 < in.size()) { + auto c = parse_hex(&in[i], 2); + if (c.has_value()) { + out += static_cast(*c); + ++i; + } else { + out += '%'; + out += in[i++]; + out += in[i]; + } + } else { + out += '%'; + out += in[i]; + } + } else if (in[i] == '+') { + out += ' '; + } else { + out += in[i]; + } + } + return out; +} + +AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { + auto find = this->params_.find(name); + if (find != this->params_.end()) { + return find->second; + } + + auto query_len = httpd_req_get_url_query_len(this->req_); + if (query_len == 0) { + return nullptr; + } + + auto query_str = std::unique_ptr(new char[++query_len]); + if (!query_str) { + ESP_LOGE(TAG, "No enough memory for get query param"); + return nullptr; + } + + auto res = httpd_req_get_url_query_str(*this, query_str.get(), query_len); + if (res != ESP_OK) { + ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); + return nullptr; + } + + auto query_val = std::unique_ptr(new char[query_len]); + if (!query_val) { + ESP_LOGE(TAG, "No enough memory for get query param value"); + return nullptr; + } + + res = httpd_query_key_value(query_str.get(), name.c_str(), query_val.get(), query_len); + if (res != ESP_OK) { + this->params_.insert({name, nullptr}); + return nullptr; + } + query_str.release(); + auto decoded = url_decode(query_val.get()); + query_val.release(); + auto *param = new AsyncWebParameter(decoded); // NOLINT(cppcoreguidelines-owning-memory) + this->params_.insert(std::make_pair(name, param)); + return param; +} + +void AsyncWebServerResponse::addHeader(const char *name, const char *value) { + httpd_resp_set_hdr(*this->req_, name, value); +} + +void AsyncResponseStream::print(float value) { this->print(to_string(value)); } + +void AsyncResponseStream::printf(const char *fmt, ...) { + std::string str; + va_list args; + + va_start(args, fmt); + size_t length = vsnprintf(nullptr, 0, fmt, args); + va_end(args); + + str.resize(length); + va_start(args, fmt); + vsnprintf(&str[0], length + 1, fmt, args); + va_end(args); + + this->print(str); +} + +AsyncEventSource::~AsyncEventSource() { + for (auto *ses : this->sessions_) { + delete ses; // NOLINT(cppcoreguidelines-owning-memory) + } +} + +void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { + auto *rsp = new AsyncEventSourceResponse(request, this); // NOLINT(cppcoreguidelines-owning-memory) + if (this->on_connect_) { + this->on_connect_(rsp); + } + this->sessions_.insert(rsp); +} + +void AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + for (auto *ses : this->sessions_) { + ses->send(message, event, id, reconnect); + } +} + +AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server) + : server_(server) { + httpd_req_t *req = *request; + + httpd_resp_set_status(req, HTTPD_200); + httpd_resp_set_type(req, "text/event-stream"); + httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); + httpd_resp_set_hdr(req, "Connection", "keep-alive"); + + httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN); + + req->sess_ctx = this; + req->free_ctx = AsyncEventSourceResponse::destroy; + + this->hd_ = req->handle; + this->fd_ = httpd_req_to_sockfd(req); +} + +void AsyncEventSourceResponse::destroy(void *ptr) { + auto *rsp = static_cast(ptr); + rsp->server_->sessions_.erase(rsp); + delete rsp; // NOLINT(cppcoreguidelines-owning-memory) +} + +void AsyncEventSourceResponse::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + if (this->fd_ == 0) { + return; + } + + std::string ev; + + if (reconnect) { + ev.append("retry: ", sizeof("retry: ") - 1); + ev.append(to_string(reconnect)); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (id) { + ev.append("id: ", sizeof("id: ") - 1); + ev.append(to_string(id)); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (event && *event) { + ev.append("event: ", sizeof("event: ") - 1); + ev.append(event); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (message && *message) { + ev.append("data: ", sizeof("data: ") - 1); + ev.append(message); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (ev.empty()) { + return; + } + + ev.append(CRLF_STR, CRLF_LEN); + + // Sending chunked content prelude + auto cs = str_snprintf("%x" CRLF_STR, 4 * sizeof(ev.size()) + CRLF_LEN, ev.size()); + httpd_socket_send(this->hd_, this->fd_, cs.c_str(), cs.size(), 0); + + // Sendiing content chunk + httpd_socket_send(this->hd_, this->fd_, ev.c_str(), ev.size(), 0); + + // Indicate end of chunk + httpd_socket_send(this->hd_, this->fd_, CRLF_STR, CRLF_LEN, 0); +} + +} // namespace web_server_idf +} // namespace esphome + +#endif // !defined(USE_ESP_IDF) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h new file mode 100644 index 0000000000..f3cecca16f --- /dev/null +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -0,0 +1,277 @@ +#pragma once +#ifdef USE_ESP_IDF + +#include + +#include +#include +#include +#include +#include + +namespace esphome { +namespace web_server_idf { + +#define F(string_literal) (string_literal) +#define PGM_P const char * +#define strncpy_P strncpy + +using String = std::string; + +class AsyncWebParameter { + public: + AsyncWebParameter(std::string value) : value_(std::move(value)) {} + const std::string &value() const { return this->value_; } + + protected: + std::string value_; +}; + +class AsyncWebServerRequest; + +class AsyncWebServerResponse { + public: + AsyncWebServerResponse(const AsyncWebServerRequest *req) : req_(req) {} + virtual ~AsyncWebServerResponse() {} + + // NOLINTNEXTLINE(readability-identifier-naming) + void addHeader(const char *name, const char *value); + + virtual const char *get_content_data() const = 0; + virtual size_t get_content_size() const = 0; + + protected: + const AsyncWebServerRequest *req_; +}; + +class AsyncWebServerResponseEmpty : public AsyncWebServerResponse { + public: + AsyncWebServerResponseEmpty(const AsyncWebServerRequest *req) : AsyncWebServerResponse(req) {} + + const char *get_content_data() const override { return nullptr; }; + size_t get_content_size() const override { return 0; }; +}; + +class AsyncWebServerResponseContent : public AsyncWebServerResponse { + public: + AsyncWebServerResponseContent(const AsyncWebServerRequest *req, std::string content) + : AsyncWebServerResponse(req), content_(std::move(content)) {} + + const char *get_content_data() const override { return this->content_.c_str(); }; + size_t get_content_size() const override { return this->content_.size(); }; + + protected: + std::string content_; +}; + +class AsyncResponseStream : public AsyncWebServerResponse { + public: + AsyncResponseStream(const AsyncWebServerRequest *req) : AsyncWebServerResponse(req) {} + + const char *get_content_data() const override { return this->content_.c_str(); }; + size_t get_content_size() const override { return this->content_.size(); }; + + void print(const char *str) { this->content_.append(str); } + void print(const std::string &str) { this->content_.append(str); } + void print(float value); + void printf(const char *fmt, ...) __attribute__((format(printf, 2, 3))); + + protected: + std::string content_; +}; + +class AsyncWebServerResponseProgmem : public AsyncWebServerResponse { + public: + AsyncWebServerResponseProgmem(const AsyncWebServerRequest *req, const uint8_t *data, const size_t size) + : AsyncWebServerResponse(req), data_(data), size_(size) {} + + const char *get_content_data() const override { return reinterpret_cast(this->data_); }; + size_t get_content_size() const override { return this->size_; }; + + protected: + const uint8_t *data_; + const size_t size_; +}; + +class AsyncWebServerRequest { + // FIXME friend class AsyncWebServerResponse; + friend class AsyncWebServer; + + public: + ~AsyncWebServerRequest(); + + http_method method() const { return static_cast(this->req_->method); } + std::string url() const; + std::string host() const; + // NOLINTNEXTLINE(readability-identifier-naming) + size_t contentLength() const { return this->req_->content_len; } + + bool authenticate(const char *username, const char *password) const; + // NOLINTNEXTLINE(readability-identifier-naming) + void requestAuthentication(const char *realm = nullptr) const; + + void redirect(const std::string &url); + + void send(AsyncWebServerResponse *response); + void send(int code, const char *content_type = nullptr, const char *content = nullptr); + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebServerResponse *beginResponse(int code, const char *content_type) { + auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, 200, content_type); + return res; + } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebServerResponse *beginResponse(int code, const char *content_type, const std::string &content) { + auto *res = new AsyncWebServerResponseContent(this, content); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, code, content_type); + return res; + } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebServerResponse *beginResponse_P(int code, const char *content_type, const uint8_t *data, + const size_t data_size) { + auto *res = new AsyncWebServerResponseProgmem(this, data, data_size); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, code, content_type); + return res; + } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncResponseStream *beginResponseStream(const char *content_type) { + auto *res = new AsyncResponseStream(this); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, 200, content_type); + return res; + } + + // NOLINTNEXTLINE(readability-identifier-naming) + bool hasParam(const std::string &name) { return this->getParam(name) != nullptr; } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebParameter *getParam(const std::string &name); + + // NOLINTNEXTLINE(readability-identifier-naming) + bool hasArg(const char *name) { return this->hasParam(name); } + std::string arg(const std::string &name) { + auto *param = this->getParam(name); + if (param) { + return param->value(); + } + return {}; + } + + operator httpd_req_t *() const { return this->req_; } + optional get_header(const char *name) const; + + protected: + httpd_req_t *req_; + AsyncWebServerResponse *rsp_{}; + std::map params_; + AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} + void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type); +}; + +class AsyncWebHandler; + +class AsyncWebServer { + public: + AsyncWebServer(uint16_t port) : port_(port){}; + ~AsyncWebServer() { this->end(); } + + // NOLINTNEXTLINE(readability-identifier-naming) + void onNotFound(std::function fn) { on_not_found_ = std::move(fn); } + + void begin(); + void end(); + + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebHandler &addHandler(AsyncWebHandler *handler) { + this->handlers_.push_back(handler); + return *handler; + } + + protected: + uint16_t port_{}; + httpd_handle_t server_{}; + static esp_err_t request_handler(httpd_req_t *r); + std::vector handlers_; + std::function on_not_found_{}; +}; + +class AsyncWebHandler { + public: + virtual ~AsyncWebHandler() {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual bool canHandle(AsyncWebServerRequest *request) { return false; } + // NOLINTNEXTLINE(readability-identifier-naming) + virtual void handleRequest(AsyncWebServerRequest *request) {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual void handleUpload(AsyncWebServerRequest *request, const std::string &filename, size_t index, uint8_t *data, + size_t len, bool final) {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual bool isRequestHandlerTrivial() { return true; } +}; + +class AsyncEventSource; + +class AsyncEventSourceResponse { + friend class AsyncEventSource; + + public: + void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + + protected: + AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server); + static void destroy(void *p); + AsyncEventSource *server_; + httpd_handle_t hd_{}; + int fd_{}; +}; + +using AsyncEventSourceClient = AsyncEventSourceResponse; + +class AsyncEventSource : public AsyncWebHandler { + friend class AsyncEventSourceResponse; + using connect_handler_t = std::function; + + public: + AsyncEventSource(std::string url) : url_(std::move(url)) {} + ~AsyncEventSource() override; + + // NOLINTNEXTLINE(readability-identifier-naming) + bool canHandle(AsyncWebServerRequest *request) override { + return request->method() == HTTP_GET && request->url() == this->url_; + } + // NOLINTNEXTLINE(readability-identifier-naming) + void handleRequest(AsyncWebServerRequest *request) override; + // NOLINTNEXTLINE(readability-identifier-naming) + void onConnect(connect_handler_t cb) { this->on_connect_ = std::move(cb); } + + void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + + protected: + std::string url_; + std::set sessions_; + connect_handler_t on_connect_{}; +}; + +class DefaultHeaders { + friend class AsyncWebServerRequest; + + public: + // NOLINTNEXTLINE(readability-identifier-naming) + void addHeader(const char *name, const char *value) { this->headers_.emplace_back(name, value); } + + // NOLINTNEXTLINE(readability-identifier-naming) + static DefaultHeaders &Instance() { + static DefaultHeaders instance; + return instance; + } + + protected: + std::vector> headers_; +}; + +} // namespace web_server_idf +} // namespace esphome + +using namespace esphome::web_server_idf; // NOLINT(google-global-names-in-headers) + +#endif // !defined(USE_ESP_IDF) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index e18d3cc043..744fc755fe 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -58,7 +58,9 @@ struct IDFWiFiEvent { wifi_event_ap_probe_req_rx_t ap_probe_req_rx; wifi_event_bss_rssi_low_t bss_rssi_low; ip_event_got_ip_t ip_got_ip; +#if LWIP_IPV6 ip_event_got_ip6_t ip_got_ip6; +#endif ip_event_ap_staipassigned_t ip_ap_staipassigned; } data; }; @@ -82,8 +84,10 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi memcpy(&event.data.sta_disconnected, event_data, sizeof(wifi_event_sta_disconnected_t)); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { memcpy(&event.data.ip_got_ip, event_data, sizeof(ip_event_got_ip_t)); +#if LWIP_IPV6 } else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) { memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t)); +#endif } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone) // no data } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { @@ -504,7 +508,9 @@ const char *get_auth_mode_str(uint8_t mode) { } std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); } +#if LWIP_IPV6 std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); } +#endif const char *get_disconnect_reason_str(uint8_t reason) { switch (reason) { case WIFI_REASON_AUTH_EXPIRE: @@ -644,9 +650,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { format_ip4_addr(it.ip_info.gw).c_str()); s_sta_got_ip = true; +#if LWIP_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; ESP_LOGV(TAG, "Event: Got IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); +#endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { ESP_LOGV(TAG, "Event: Lost IP"); diff --git a/esphome/components/xl9535/__init__.py b/esphome/components/xl9535/__init__.py new file mode 100644 index 0000000000..7fcac50ba7 --- /dev/null +++ b/esphome/components/xl9535/__init__.py @@ -0,0 +1,73 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, +) +from esphome import pins + +CONF_XL9535 = "xl9535" + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@mreditor97"] + +xl9535_ns = cg.esphome_ns.namespace(CONF_XL9535) + +XL9535Component = xl9535_ns.class_("XL9535Component", cg.Component, i2c.I2CDevice) +XL9535GPIOPin = xl9535_ns.class_("XL9535GPIOPin", cg.GPIOPin) + +MULTI_CONF = True +CONFIG_SCHEMA = ( + cv.Schema({cv.Required(CONF_ID): cv.declare_id(XL9535Component)}) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x20)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + +def validate_mode(mode): + if not (mode[CONF_INPUT] or mode[CONF_OUTPUT]) or ( + mode[CONF_INPUT] and mode[CONF_OUTPUT] + ): + raise cv.Invalid("Mode must be either a input or a output") + return mode + + +XL9535_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(XL9535GPIOPin), + cv.Required(CONF_XL9535): cv.use_id(XL9535Component), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_XL9535, XL9535_PIN_SCHEMA) +async def xl9535_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_XL9535]) + + cg.add(var.set_parent(parent)) + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + + return var diff --git a/esphome/components/xl9535/xl9535.cpp b/esphome/components/xl9535/xl9535.cpp new file mode 100644 index 0000000000..8f4f556b4a --- /dev/null +++ b/esphome/components/xl9535/xl9535.cpp @@ -0,0 +1,122 @@ +#include "xl9535.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace xl9535 { + +static const char *const TAG = "xl9535"; + +void XL9535Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up XL9535..."); + + // Check to see if the device can read from the register + uint8_t port = 0; + if (this->read_register(XL9535_INPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } +} + +void XL9535Component::dump_config() { + ESP_LOGCONFIG(TAG, "XL9535:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with XL9535 failed!"); + } +} + +bool XL9535Component::digital_read(uint8_t pin) { + bool state = false; + uint8_t port = 0; + + if (pin > 7) { + if (this->read_register(XL9535_INPUT_PORT_1_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return state; + } + + state = (port & (pin - 10)) != 0; + } else { + if (this->read_register(XL9535_INPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return state; + } + + state = (port & pin) != 0; + } + + this->status_clear_warning(); + return state; +} + +void XL9535Component::digital_write(uint8_t pin, bool value) { + uint8_t port = 0; + uint8_t register_data = 0; + + if (pin > 7) { + if (this->read_register(XL9535_OUTPUT_PORT_1_REGISTER, ®ister_data, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + register_data = register_data & (~(1 << (pin - 10))); + port = register_data | value << (pin - 10); + + if (this->write_register(XL9535_OUTPUT_PORT_1_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + } else { + if (this->read_register(XL9535_OUTPUT_PORT_0_REGISTER, ®ister_data, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + register_data = register_data & (~(1 << pin)); + port = register_data | value << pin; + + if (this->write_register(XL9535_OUTPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + } + + this->status_clear_warning(); +} + +void XL9535Component::pin_mode(uint8_t pin, gpio::Flags mode) { + uint8_t port = 0; + + if (pin > 7) { + this->read_register(XL9535_CONFIG_PORT_1_REGISTER, &port, 1); + + if (mode == gpio::FLAG_INPUT) { + port = port | (1 << (pin - 10)); + } else if (mode == gpio::FLAG_OUTPUT) { + port = port & (~(1 << (pin - 10))); + } + + this->write_register(XL9535_CONFIG_PORT_1_REGISTER, &port, 1); + } else { + this->read_register(XL9535_CONFIG_PORT_0_REGISTER, &port, 1); + + if (mode == gpio::FLAG_INPUT) { + port = port | (1 << pin); + } else if (mode == gpio::FLAG_OUTPUT) { + port = port & (~(1 << pin)); + } + + this->write_register(XL9535_CONFIG_PORT_0_REGISTER, &port, 1); + } +} + +void XL9535GPIOPin::setup() { this->pin_mode(this->flags_); } + +std::string XL9535GPIOPin::dump_summary() const { return str_snprintf("%u via XL9535", 15, this->pin_); } + +void XL9535GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool XL9535GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void XL9535GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace xl9535 +} // namespace esphome diff --git a/esphome/components/xl9535/xl9535.h b/esphome/components/xl9535/xl9535.h new file mode 100644 index 0000000000..8f0a868c42 --- /dev/null +++ b/esphome/components/xl9535/xl9535.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace xl9535 { + +enum { + XL9535_INPUT_PORT_0_REGISTER = 0x00, + XL9535_INPUT_PORT_1_REGISTER = 0x01, + XL9535_OUTPUT_PORT_0_REGISTER = 0x02, + XL9535_OUTPUT_PORT_1_REGISTER = 0x03, + XL9535_INVERSION_PORT_0_REGISTER = 0x04, + XL9535_INVERSION_PORT_1_REGISTER = 0x05, + XL9535_CONFIG_PORT_0_REGISTER = 0x06, + XL9535_CONFIG_PORT_1_REGISTER = 0x07, +}; + +class XL9535Component : public Component, public i2c::I2CDevice { + public: + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, gpio::Flags mode); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } +}; + +class XL9535GPIOPin : public GPIOPin { + public: + void set_parent(XL9535Component *parent) { this->parent_ = parent; } + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + + void setup() override; + std::string dump_summary() const override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + XL9535Component *parent_; + + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace xl9535 +} // namespace esphome diff --git a/esphome/components/zio_ultrasonic/__init__.py b/esphome/components/zio_ultrasonic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/zio_ultrasonic/sensor.py b/esphome/components/zio_ultrasonic/sensor.py new file mode 100644 index 0000000000..c5eed14e64 --- /dev/null +++ b/esphome/components/zio_ultrasonic/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_DISTANCE, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@kahrendt"] + +zio_ultrasonic_ns = cg.esphome_ns.namespace("zio_ultrasonic") + +ZioUltrasonicComponent = zio_ultrasonic_ns.class_( + "ZioUltrasonicComponent", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + ZioUltrasonicComponent, + unit_of_measurement="mm", + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x00)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp b/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp new file mode 100644 index 0000000000..565bbe9b4f --- /dev/null +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp @@ -0,0 +1,31 @@ + +#include "zio_ultrasonic.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace zio_ultrasonic { + +static const char *const TAG = "zio_ultrasonic"; + +void ZioUltrasonicComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Zio Ultrasonic Sensor:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Sensor:", this); +} + +void ZioUltrasonicComponent::update() { + uint16_t distance; + + // Read an unsigned two byte integerfrom register 0x01 which gives distance in mm + if (!this->read_byte_16(0x01, &distance)) { + ESP_LOGE(TAG, "Error reading data from Zio Ultrasonic"); + this->publish_state(NAN); + } else { + this->publish_state(distance); + } +} + +} // namespace zio_ultrasonic +} // namespace esphome diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.h b/esphome/components/zio_ultrasonic/zio_ultrasonic.h new file mode 100644 index 0000000000..84c8d44c65 --- /dev/null +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +static const char *const TAG = "Zio Ultrasonic"; + +namespace esphome { +namespace zio_ultrasonic { + +class ZioUltrasonicComponent : public i2c::I2CDevice, public PollingComponent, public sensor::Sensor { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + + void dump_config() override; + + void update() override; +}; + +} // namespace zio_ultrasonic +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index ea660723e4..f04e19c359 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.6.5" +__version__ = "2023.7.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -262,6 +262,7 @@ CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" +CONF_FLOW = "flow" CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" @@ -286,6 +287,7 @@ CONF_GPIO = "gpio" CONF_GREEN = "green" CONF_GROUP = "group" CONF_HARDWARE_UART = "hardware_uart" +CONF_HEAD = "head" CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" CONF_HEAT_DEADBAND = "heat_deadband" @@ -891,6 +893,7 @@ UNIT_CENTIMETER = "cm" UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" UNIT_CUBIC_METER = "m³" +UNIT_CUBIC_METER_PER_HOUR = "m³/h" UNIT_DECIBEL = "dB" UNIT_DECIBEL_MILLIWATT = "dBm" UNIT_DEGREE_PER_SECOND = "°/s" @@ -928,6 +931,7 @@ UNIT_PERCENT = "%" UNIT_PH = "pH" UNIT_PULSES = "pulses" UNIT_PULSES_PER_MINUTE = "pulses/min" +UNIT_REVOLUTIONS_PER_MINUTE = "RPM" UNIT_SECOND = "s" UNIT_STEPS = "steps" UNIT_VOLT = "V" diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 49ef8ecde7..ae85d55498 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -201,8 +201,8 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() { uint32_t now = millis(); if (now - started_ > 50) { const char *src = component_ == nullptr ? "" : component_->get_component_source(); - ESP_LOGV(TAG, "Component %s took a long time for an operation (%.2f s).", src, (now - started_) / 1e3f); - ESP_LOGV(TAG, "Components should block for at most 20-30ms."); + ESP_LOGW(TAG, "Component %s took a long time for an operation (%.2f s).", src, (now - started_) / 1e3f); + ESP_LOGW(TAG, "Components should block for at most 20-30ms."); ; } } diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 7c76c8490b..012c9af3c6 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -29,7 +29,7 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u if (timeout == SCHEDULER_DONT_RUN) return; - ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout); + ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%" PRIu32 ")", name.c_str(), timeout); auto item = make_unique(); item->component = component; @@ -60,7 +60,7 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name, if (interval != 0) offset = (random_uint32() % interval) / 2; - ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset); + ESP_LOGVV(TAG, "set_interval(name='%s', interval=%" PRIu32 ", offset=%" PRIu32 ")", name.c_str(), interval, offset); auto item = make_unique(); item->component = component; @@ -108,8 +108,8 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin if (initial_wait_time == SCHEDULER_DONT_RUN) return; - ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u, max_attempts=%u, backoff_factor=%0.1f)", name.c_str(), - initial_wait_time, max_attempts, backoff_increase_factor); + ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%0.1f)", + name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); if (backoff_increase_factor < 0.0001) { ESP_LOGE(TAG, @@ -154,16 +154,16 @@ void HOT Scheduler::call() { if (now - last_print > 2000) { last_print = now; std::vector> old_items; - ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now); + ESP_LOGVV(TAG, "Items: count=%u, now=%" PRIu32, this->items_.size(), now); while (!this->empty_()) { this->lock_.lock(); auto item = std::move(this->items_[0]); this->pop_raw_(); this->lock_.unlock(); - ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", item->get_type_str(), - item->name.c_str(), item->interval, item->last_execution, item->last_execution_major, - item->next_execution(), item->next_execution_major()); + ESP_LOGVV(TAG, " %s '%s' interval=%" PRIu32 " last_execution=%" PRIu32 " (%u) next=%" PRIu32 " (%u)", + item->get_type_str(), item->name.c_str(), item->interval, item->last_execution, + item->last_execution_major, item->next_execution(), item->next_execution_major()); old_items.push_back(std::move(item)); } @@ -222,8 +222,8 @@ void HOT Scheduler::call() { } #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", item->get_type_str(), - item->name.c_str(), item->interval, item->last_execution, now); + ESP_LOGVV(TAG, "Running %s '%s' with interval=%" PRIu32 " last_execution=%" PRIu32 " (now=%" PRIu32 ")", + item->get_type_str(), item->name.c_str(), item->interval, item->last_execution, now); #endif // Warning: During callback(), a lot of stuff can happen, including: diff --git a/platformio.ini b/platformio.ini index 3565b15809..b970ef7a69 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,6 +39,7 @@ lib_deps = bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 + pavlodn/HaierProtocol@0.9.18 ; haier ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library build_flags = @@ -61,7 +62,7 @@ lib_deps = fastled/FastLED@3.3.2 ; fastled_base mikalhart/TinyGPSPlus@1.0.2 ; gps freekode/TM1651@1.0.1 ; tm1651 - glmnet/Dsmr@0.5 ; dsmr + glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr dudanov/MideaUART@1.1.8 ; midea tonia/HeatpumpIR@1.0.20 ; heatpumpir @@ -156,7 +157,7 @@ board_build.filesystem_size = 0.5m platform = https://github.com/maxgerhardt/platform-raspberrypi.git platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/2.6.2/rp2040-2.6.2.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.3.0/rp2040-3.3.0.zip framework = arduino lib_deps = diff --git a/requirements.txt b/requirements.txt index 934e0b46a6..74c15c9213 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ voluptuous==0.13.1 -PyYAML==6.0 +PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 tornado==6.3.2 @@ -7,11 +7,11 @@ tzlocal==5.0.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.7 # When updating platformio, also update Dockerfile -esptool==4.6 +esptool==4.6.2 click==8.1.3 -esphome-dashboard==20230621.0 -aioesphomeapi==14.0.0 -zeroconf==0.63.0 +esphome-dashboard==20230711.0 +aioesphomeapi==15.0.0 +zeroconf==0.69.0 # 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_optional.txt b/requirements_optional.txt index df6b3b387e..8bbf0a6809 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,3 +1,3 @@ -pillow>4.0.0 +pillow>4.0.0,<10.0.0 cairosvg>=2.2.0 cryptography>=2.0.0,<4 diff --git a/requirements_test.txt b/requirements_test.txt index d5235d733b..75f29ac8dd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ pylint==2.17.4 flake8==6.0.0 # also change in .pre-commit-config.yaml when updating black==23.3.0 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.4.0 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.7.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==7.3.1 +pytest==7.4.0 pytest-cov==4.1.0 -pytest-mock==3.10.0 +pytest-mock==3.11.1 pytest-asyncio==0.21.0 asyncmock==0.4.2 hypothesis==5.49.0 diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 2ee7521b91..22f3c1b4bc 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -7,6 +7,7 @@ from collections import defaultdict from esphome.helpers import write_file_if_changed from esphome.config import get_component, get_platform from esphome.core import CORE +from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK parser = argparse.ArgumentParser() parser.add_argument( @@ -38,6 +39,7 @@ parts = [BASE] # Fake some directory so that get_component works CORE.config_path = str(root) +CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None} codeowners = defaultdict(list) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index dd8eccde93..c6fcf5eb64 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -461,8 +461,10 @@ def merge(source, destination): def is_platform_schema(schema_name): # added mostly because of schema_name == "microphone.MICROPHONE_SCHEMA" + # and "alarm_control_panel" # which is shrunk because there is only one component of the schema (i2s_audio) - return schema_name == "microphone.MICROPHONE_SCHEMA" + component = schema_name.split(".")[0] + return component in components and components[component].is_platform_component def shrink(): @@ -530,6 +532,10 @@ def shrink(): elif not key_s: for target in paths: target_s = get_arr_path_schema(target) + if S_SCHEMA not in target_s: + # an empty schema like speaker.SPEAKER_SCHEMA + target_s[S_EXTENDS].remove(x) + continue assert target_s[S_SCHEMA][S_EXTENDS] == [x] target_s.pop(S_SCHEMA) target_s.pop(S_TYPE) # undefined diff --git a/script/ci-custom.py b/script/ci-custom.py index 44ed83f392..a731e2e5b8 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -607,7 +607,7 @@ def lint_trailing_whitespace(fname, match): "esphome/components/button/button.h", "esphome/components/climate/climate.h", "esphome/components/cover/cover.h", - "esphome/components/display/display_buffer.h", + "esphome/components/display/display.h", "esphome/components/fan/fan.h", "esphome/components/i2c/i2c.h", "esphome/components/lock/lock.h", diff --git a/script/test b/script/test index 36be9118ed..36a58cd75a 100755 --- a/script/test +++ b/script/test @@ -9,6 +9,9 @@ set -x esphome compile tests/test1.yaml esphome compile tests/test2.yaml esphome compile tests/test3.yaml +esphome compile tests/test3.1.yaml esphome compile tests/test4.yaml esphome compile tests/test5.yaml +esphome compile tests/test6.yaml +esphome compile tests/test7.yaml esphome compile tests/test8.yaml diff --git a/tests/test1.yaml b/tests/test1.yaml index f8928430f4..bf099e2844 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -325,6 +325,7 @@ ble_client: accept: True - mac_address: C4:4F:33:11:22:33 id: my_bedjet_ble_client + bedjet: - ble_client_id: my_bedjet_ble_client id: my_bedjet_client @@ -482,6 +483,25 @@ sensor: nir: name: NIR i2c_id: i2c_bus + - platform: atm90e26 + cs_pin: 5 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 - platform: atm90e32 cs_pin: 5 phase_a: @@ -1269,6 +1289,16 @@ sensor: pressure: name: "MPL3115A2 Pressure" update_interval: 10s + - platform: alpha3 + ble_client_id: ble_foo + flow: + name: "Radiator Pump Flow" + head: + name: "Radiator Pump Head" + power: + name: "Radiator Pump Power" + speed: + name: "Radiator Pump Speed" - platform: ld2410 moving_distance: name: "Moving distance (cm)" @@ -1310,6 +1340,10 @@ sensor: fault_count: 1 polarity: active_high function: comparator + - platform: zio_ultrasonic + name: "Distance" + update_interval: 60s + i2c_id: i2c_bus esp32_touch: setup_mode: false @@ -1355,8 +1389,15 @@ binary_sensor: device_class: window filters: - invert: + - delayed_on_off: 40ms + - delayed_on_off: + time_on: 10s + time_off: !lambda "return 1000;" - delayed_on: 40ms - delayed_off: 40ms + - delayed_on_off: !lambda "return 10;" + - delayed_on: !lambda "return 1000;" + - delayed_off: !lambda "return 0;" on_press: then: - lambda: >- @@ -2434,7 +2475,6 @@ switch: level: !lambda "return 0.5;" turn_off_action: - switch.turn_on: living_room_lights_off - restore_state: false on_turn_on: - switch.template.publish: id: livingroom_lights @@ -2470,7 +2510,6 @@ switch: } optimistic: true assumed_state: false - restore_state: true on_turn_off: - switch.template.publish: id: my_switch diff --git a/tests/test2.yaml b/tests/test2.yaml index aa3e467816..291dc240dc 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -316,6 +316,7 @@ sensor: platform: airthings_wave_plus ble_client_id: airthings01 update_interval: 5min + battery_update_interval: 12h temperature: name: Wave Plus Temperature radon: @@ -330,10 +331,13 @@ sensor: name: Wave Plus CO2 tvoc: name: Wave Plus VOC + battery_voltage: + name: Wave Plus Battery Voltage - id: airthingswm platform: airthings_wave_mini ble_client_id: airthingsmini01 update_interval: 5min + battery_update_interval: 12h temperature: name: Wave Mini Temperature humidity: @@ -342,6 +346,8 @@ sensor: name: Wave Mini Pressure tvoc: name: Wave Mini VOC + battery_voltage: + name: Wave Mini Battery Voltage - platform: ina260 address: 0x40 current: @@ -397,6 +403,31 @@ sensor: name: MICS-4514 C2H5OH ammonia: name: MICS-4514 NH3 + - platform: mopeka_std_check + mac_address: D3:75:F2:DC:16:91 + tank_type: CUSTOM + custom_distance_full: 40cm + custom_distance_empty: 10mm + temperature: + name: Propane test temp + level: + name: Propane test level + distance: + name: Propane test distance + battery_level: + name: Propane test battery level + - platform: duty_time + id: duty_time1 + name: Test Duty Time + restore: true + last_time: + name: Test Last Duty Time Sensor + sensor: ha_hello_world_binary + - platform: duty_time + id: duty_time2 + name: Test Duty Time 2 + restore: false + lambda: "return true;" time: - platform: homeassistant @@ -404,6 +435,17 @@ time: - at: "16:00:00" then: - logger.log: It's 16:00 + - if: + condition: + - sensor.duty_time.is_running: duty_time2 + then: + - sensor.duty_time.start: duty_time1 + - if: + condition: + - sensor.duty_time.is_not_running: duty_time1 + then: + - sensor.duty_time.stop: duty_time2 + - sensor.duty_time.reset: duty_time1 esp32_touch: setup_mode: true diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 0daf4e9671..104f4bbda8 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -86,6 +86,7 @@ sensor: - delta: 100 - throttle: 100ms - debounce: 500s + - timeout: 10min - calibrate_linear: - 0 -> 0 - 100 -> 100 @@ -303,6 +304,10 @@ sm2135: rgb_current: 20mA cw_current: 60mA +grove_tb6612fng: + id: test_motor + address: 0x14 + switch: - platform: template name: mpr121_toggle @@ -353,6 +358,17 @@ switch: Content-Type: application/json body: Some data verify_ssl: false + - platform: template + name: open_vent + id: open_vent + optimistic: True + on_turn_on: + then: + - grove_tb6612fng.run: + channel: 1 + speed: 255 + direction: BACKWARD + id: test_motor custom_component: diff --git a/tests/test3.yaml b/tests/test3.yaml index 8307ac2984..f7b66a748e 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -944,13 +944,29 @@ climate: kd_multiplier: 0.0 deadband_output_averaging_samples: 1 - platform: haier + protocol: hOn name: Haier AC - supported_swing_modes: - - vertical - - horizontal - - both - update_interval: 10s uart_id: uart_12 + wifi_signal: true + beeper: true + outdoor_temperature: + name: Haier AC outdoor temperature + visual: + min_temperature: 16 °C + max_temperature: 30 °C + temperature_step: 1 °C + supported_modes: + - 'OFF' + - AUTO + - COOL + - HEAT + - DRY + - FAN_ONLY + supported_swing_modes: + - 'OFF' + - VERTICAL + - HORIZONTAL + - BOTH sprinkler: - id: yard_sprinkler_ctrlr diff --git a/tests/test4.yaml b/tests/test4.yaml index 8e76a5fd66..2a8cb02413 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -384,6 +384,15 @@ binary_sensor: pullup: true inverted: false + - platform: gpio + name: XL9535 Pin 0 + pin: + xl9535: xl9535_hub + number: 0 + mode: + input: true + inverted: false + climate: - platform: tuya id: tuya_climate @@ -495,6 +504,14 @@ display: full_update_every: 30 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: GPIO23 + dc_pin: GPIO23 + busy_pin: GPIO23 + reset_pin: GPIO23 + model: 1.54in-m5coreink-m09 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: inkplate6 id: inkplate_display greyscale: false @@ -745,3 +762,7 @@ voice_assistant: max6956: - id: max6956_1 address: 0x40 + +xl9535: + - id: xl9535_hub + address: 0x20 diff --git a/tests/test5.yaml b/tests/test5.yaml index cb4b559b06..a2530d799a 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -590,6 +590,7 @@ display: time: - platform: pcf85063 + - platform: pcf8563 text_sensor: - platform: ezo_pmp diff --git a/tests/test8.yaml b/tests/test8.yaml index 2430a0d1e6..8d031b033f 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -18,10 +18,35 @@ light: - platform: neopixelbus type: GRB variant: WS2812 - pin: 33 + pin: GPIO38 num_leds: 1 id: neopixel method: esp32_rmt name: neopixel-enable internal: false restore_mode: ALWAYS_OFF + +spi: + clk_pin: GPIO7 + mosi_pin: GPIO6 + +display: + - platform: ili9xxx + model: ili9342 + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: GPIO48 + +i2c: + scl: GPIO18 + sda: GPIO8 + +touchscreen: + - platform: tt21100 + interrupt_pin: GPIO3 + reset_pin: GPIO48 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1