mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	
							
								
								
									
										1
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -16,6 +16,7 @@ Quick description and explanation of changes | ||||
| ## Test Environment | ||||
|  | ||||
| - [ ] ESP32 | ||||
| - [ ] ESP32 IDF | ||||
| - [ ] ESP8266 | ||||
|  | ||||
| ## Example entry for `config.yaml`: | ||||
|   | ||||
							
								
								
									
										7
									
								
								.github/issue-close-app.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/issue-close-app.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,7 +0,0 @@ | ||||
| comment: >- | ||||
|   https://github.com/esphome/esphome/issues/430 | ||||
| issueConfigs: | ||||
| - content: | ||||
|   - "OTHERWISE THE ISSUE WILL BE CLOSED AUTOMATICALLY" | ||||
|  | ||||
| caseInsensitive: false | ||||
							
								
								
									
										36
									
								
								.github/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								.github/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,36 +0,0 @@ | ||||
| # Configuration for Lock Threads - https://github.com/dessant/lock-threads | ||||
|  | ||||
| # Number of days of inactivity before a closed issue or pull request is locked | ||||
| daysUntilLock: 7 | ||||
|  | ||||
| # Skip issues and pull requests created before a given timestamp. Timestamp must | ||||
| # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable | ||||
| skipCreatedBefore: false | ||||
|  | ||||
| # Issues and pull requests with these labels will be ignored. Set to `[]` to disable | ||||
| exemptLabels: | ||||
|   - keep-open | ||||
|  | ||||
| # Label to add before locking, such as `outdated`. Set to `false` to disable | ||||
| lockLabel: false | ||||
|  | ||||
| # Comment to post before locking. Set to `false` to disable | ||||
| lockComment: false | ||||
|  | ||||
| # Assign `resolved` as the reason for locking. Set to `false` to disable | ||||
| setLockReason: false | ||||
|  | ||||
| # Limit to only `issues` or `pulls` | ||||
| # only: issues | ||||
|  | ||||
| # Optionally, specify configuration settings just for `issues` or `pulls` | ||||
| # issues: | ||||
| #   exemptLabels: | ||||
| #     - help-wanted | ||||
| #   lockLabel: outdated | ||||
|  | ||||
| # pulls: | ||||
| #   daysUntilLock: 30 | ||||
|  | ||||
| # Repository to extend settings from | ||||
| # _extends: repo | ||||
							
								
								
									
										4
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,6 +17,10 @@ on: | ||||
|       - 'requirements*.txt' | ||||
|       - 'platformio.ini' | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|   packages: read | ||||
|  | ||||
| jobs: | ||||
|   check-docker: | ||||
|     name: Build docker containers | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -8,6 +8,9 @@ on: | ||||
|  | ||||
|   pull_request: | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   ci: | ||||
|     name: ${{ matrix.name }} | ||||
|   | ||||
							
								
								
									
										14
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,13 +9,19 @@ permissions: | ||||
|   issues: write | ||||
|   pull-requests: write | ||||
|  | ||||
| concurrency: | ||||
|   group: lock | ||||
|  | ||||
| jobs: | ||||
|   lock: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: dessant/lock-threads@v2 | ||||
|       - uses: dessant/lock-threads@v3 | ||||
|         with: | ||||
|           github-token: ${{ github.token }} | ||||
|           pr-lock-inactive-days: "1" | ||||
|           pr-inactive-days: "1" | ||||
|           pr-lock-reason: "" | ||||
|           process-only: prs | ||||
|           exclude-any-pr-labels: keep-open | ||||
|  | ||||
|           issue-inactive-days: "7" | ||||
|           issue-lock-reason: "" | ||||
|           exclude-any-issue-labels: keep-open | ||||
|   | ||||
							
								
								
									
										9
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -7,6 +7,9 @@ on: | ||||
|   schedule: | ||||
|     - cron: "0 2 * * *" | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   init: | ||||
|     name: Initialize build | ||||
| @@ -52,6 +55,9 @@ jobs: | ||||
|   deploy-docker: | ||||
|     name: Build and publish docker containers | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     permissions: | ||||
|       contents: read | ||||
|       packages: write | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [init] | ||||
|     strategy: | ||||
| @@ -93,6 +99,9 @@ jobs: | ||||
|  | ||||
|   deploy-docker-manifest: | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     permissions: | ||||
|       contents: read | ||||
|       packages: write | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [init, deploy-docker] | ||||
|     strategy: | ||||
|   | ||||
							
								
								
									
										20
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,13 +9,15 @@ permissions: | ||||
|   issues: write | ||||
|   pull-requests: write | ||||
|  | ||||
| concurrency: | ||||
|   group: lock | ||||
|  | ||||
| jobs: | ||||
|   stale: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v4 | ||||
|         with: | ||||
|           repo-token: ${{ github.token }} | ||||
|           days-before-pr-stale: 90 | ||||
|           days-before-pr-close: 7 | ||||
|           days-before-issue-stale: -1 | ||||
| @@ -28,3 +30,19 @@ jobs: | ||||
|             pull request has been automatically marked as stale because of that | ||||
|             and will be closed if no further activity occurs within 7 days. | ||||
|             Thank you for your contributions. | ||||
|  | ||||
|   # Use stale to automatically close issues with a reference to the issue tracker | ||||
|   close-issues: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v4 | ||||
|         with: | ||||
|           days-before-pr-stale: -1 | ||||
|           days-before-pr-close: -1 | ||||
|           days-before-issue-stale: 1 | ||||
|           days-before-issue-close: 1 | ||||
|           remove-stale-when-updated: true | ||||
|           stale-issue-label: "stale" | ||||
|           exempt-issue-labels: "not-stale" | ||||
|           stale-issue-message: > | ||||
|             https://github.com/esphome/esphome/issues/430 | ||||
|   | ||||
| @@ -31,6 +31,7 @@ esphome/components/binary_sensor/* @esphome/core | ||||
| esphome/components/ble_client/* @buxtronix | ||||
| esphome/components/bme680_bsec/* @trvrnrth | ||||
| esphome/components/canbus/* @danielschramm @mvturnho | ||||
| esphome/components/cap1188/* @MrEditor97 | ||||
| esphome/components/captive_portal/* @OttoWinter | ||||
| esphome/components/ccs811/* @habbie | ||||
| esphome/components/climate/* @esphome/core | ||||
| @@ -39,6 +40,7 @@ esphome/components/color_temperature/* @jesserockz | ||||
| esphome/components/coolix/* @glmnet | ||||
| esphome/components/cover/* @esphome/core | ||||
| esphome/components/cs5460a/* @balrog-kun | ||||
| esphome/components/cse7761/* @berfenger | ||||
| esphome/components/ct_clamp/* @jesserockz | ||||
| esphome/components/current_based/* @djwmarcx | ||||
| esphome/components/daly_bms/* @s1lvi0 | ||||
| @@ -51,6 +53,7 @@ esphome/components/dsmr/* @glmnet @zuidwijk | ||||
| esphome/components/esp32/* @esphome/core | ||||
| esphome/components/esp32_ble/* @jesserockz | ||||
| esphome/components/esp32_ble_server/* @jesserockz | ||||
| esphome/components/esp32_camera_web_server/* @ayufan | ||||
| esphome/components/esp32_improv/* @jesserockz | ||||
| esphome/components/esp8266/* @esphome/core | ||||
| esphome/components/exposure_notifications/* @OttoWinter | ||||
| @@ -70,6 +73,7 @@ esphome/components/homeassistant/* @OttoWinter | ||||
| esphome/components/hrxl_maxsonar_wr/* @netmikey | ||||
| esphome/components/i2c/* @esphome/core | ||||
| esphome/components/improv/* @jesserockz | ||||
| esphome/components/improv_serial/* @esphome/core | ||||
| esphome/components/inkbird_ibsth1_mini/* @fkirill | ||||
| esphome/components/inkplate6/* @jesserockz | ||||
| esphome/components/integration/* @OttoWinter | ||||
|   | ||||
| @@ -5,12 +5,12 @@ | ||||
| # One of "docker", "hassio" | ||||
| ARG BASEIMGTYPE=docker | ||||
|  | ||||
| FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.0 AS base-hassio-amd64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.0 AS base-hassio-arm64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.0 AS base-hassio-armv7 | ||||
| FROM debian:bullseye-20210902-slim AS base-docker-amd64 | ||||
| FROM debian:bullseye-20210902-slim AS base-docker-arm64 | ||||
| FROM debian:bullseye-20210902-slim AS base-docker-armv7 | ||||
| FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.1 AS base-hassio-amd64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.1 AS base-hassio-arm64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.1 AS base-hassio-armv7 | ||||
| FROM debian:bullseye-20211011-slim AS base-docker-amd64 | ||||
| FROM debian:bullseye-20211011-slim AS base-docker-arm64 | ||||
| FROM debian:bullseye-20211011-slim AS base-docker-armv7 | ||||
|  | ||||
| # Use TARGETARCH/TARGETVARIANT defined by docker | ||||
| # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope | ||||
| @@ -43,7 +43,7 @@ RUN \ | ||||
|     # Ubuntu python3-pip is missing wheel | ||||
|     pip3 install --no-cache-dir \ | ||||
|         wheel==0.36.2 \ | ||||
|         platformio==5.2.1 \ | ||||
|         platformio==5.2.2 \ | ||||
|     # Change some platformio settings | ||||
|     && platformio settings set enable_telemetry No \ | ||||
|     && platformio settings set check_libraries_interval 1000000 \ | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_AUTOMATION_ID, | ||||
|     CONF_CONDITION, | ||||
|     CONF_COUNT, | ||||
|     CONF_ELSE, | ||||
|     CONF_ID, | ||||
|     CONF_THEN, | ||||
| @@ -66,6 +67,7 @@ DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) | ||||
| LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) | ||||
| IfAction = cg.esphome_ns.class_("IfAction", Action) | ||||
| WhileAction = cg.esphome_ns.class_("WhileAction", Action) | ||||
| RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) | ||||
| WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component) | ||||
| UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action) | ||||
| Automation = cg.esphome_ns.class_("Automation") | ||||
| @@ -241,6 +243,25 @@ async def while_action_to_code(config, action_id, template_arg, args): | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @register_action( | ||||
|     "repeat", | ||||
|     RepeatAction, | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_COUNT): cv.templatable(cv.positive_not_null_int), | ||||
|             cv.Required(CONF_THEN): validate_action_list, | ||||
|         } | ||||
|     ), | ||||
| ) | ||||
| async def repeat_action_to_code(config, action_id, template_arg, args): | ||||
|     var = cg.new_Pvariable(action_id, template_arg) | ||||
|     count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32) | ||||
|     cg.add(var.set_count(count_template)) | ||||
|     actions = await build_action_list(config[CONF_THEN], template_arg, args) | ||||
|     cg.add(var.add_then(actions)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| def validate_wait_until(value): | ||||
|     schema = cv.Schema( | ||||
|         { | ||||
|   | ||||
| @@ -81,4 +81,5 @@ from esphome.cpp_types import (  # noqa | ||||
|     GPIOPin, | ||||
|     InternalGPIOPin, | ||||
|     gpio_Flags, | ||||
|     EntityCategory, | ||||
| ) | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #include "adc_sensor.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
| @@ -15,50 +16,6 @@ namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc"; | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } | ||||
|  | ||||
| inline adc1_channel_t gpio_to_adc1(uint8_t pin) { | ||||
| #if CONFIG_IDF_TARGET_ESP32 | ||||
|   switch (pin) { | ||||
|     case 36: | ||||
|       return ADC1_CHANNEL_0; | ||||
|     case 37: | ||||
|       return ADC1_CHANNEL_1; | ||||
|     case 38: | ||||
|       return ADC1_CHANNEL_2; | ||||
|     case 39: | ||||
|       return ADC1_CHANNEL_3; | ||||
|     case 32: | ||||
|       return ADC1_CHANNEL_4; | ||||
|     case 33: | ||||
|       return ADC1_CHANNEL_5; | ||||
|     case 34: | ||||
|       return ADC1_CHANNEL_6; | ||||
|     case 35: | ||||
|       return ADC1_CHANNEL_7; | ||||
|     default: | ||||
|       return ADC1_CHANNEL_MAX; | ||||
|   } | ||||
| #elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 | ||||
|   switch (pin) { | ||||
|     case 0: | ||||
|       return ADC1_CHANNEL_0; | ||||
|     case 1: | ||||
|       return ADC1_CHANNEL_1; | ||||
|     case 2: | ||||
|       return ADC1_CHANNEL_2; | ||||
|     case 3: | ||||
|       return ADC1_CHANNEL_3; | ||||
|     case 4: | ||||
|       return ADC1_CHANNEL_4; | ||||
|     default: | ||||
|       return ADC1_CHANNEL_MAX; | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); | ||||
| #ifndef USE_ADC_SENSOR_VCC | ||||
| @@ -66,13 +23,36 @@ void ADCSensor::setup() { | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   adc1_config_channel_atten(gpio_to_adc1(pin_->get_pin()), attenuation_); | ||||
|   adc1_config_width(ADC_WIDTH_BIT_12); | ||||
| #if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32H2 | ||||
|   adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_->get_pin())); | ||||
| #endif | ||||
|   if (!autorange_) { | ||||
|     adc1_config_channel_atten(channel_, attenuation_); | ||||
|   } | ||||
|  | ||||
|   // load characteristics for each attenuation | ||||
|   for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) { | ||||
|     auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_BIT_12, | ||||
|                                               1100,  // default vref | ||||
|                                               &cal_characteristics_[i]); | ||||
|     switch (cal_value) { | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_VREF: | ||||
|         ESP_LOGV(TAG, "Using eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_EFUSE_TP: | ||||
|         ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); | ||||
|         break; | ||||
|       case ESP_ADC_CAL_VAL_DEFAULT_VREF: | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2 | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) | ||||
|   adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); | ||||
| #endif | ||||
| #endif  // USE_ESP32 | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
| #ifdef USE_ESP8266 | ||||
| @@ -81,84 +61,107 @@ void ADCSensor::dump_config() { | ||||
| #else | ||||
|   LOG_PIN("  Pin: ", pin_); | ||||
| #endif | ||||
| #endif | ||||
| #endif  // USE_ESP8266 | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   LOG_PIN("  Pin: ", pin_); | ||||
|   switch (this->attenuation_) { | ||||
|     case ADC_ATTEN_DB_0: | ||||
|       ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_2_5: | ||||
|       ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_6: | ||||
|       ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_11: | ||||
|       ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); | ||||
|       break; | ||||
|     default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|       break; | ||||
|   } | ||||
| #endif | ||||
|   if (autorange_) | ||||
|     ESP_LOGCONFIG(TAG, " Attenuation: auto"); | ||||
|   else | ||||
|     switch (this->attenuation_) { | ||||
|       case ADC_ATTEN_DB_0: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_2_5: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_6: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_11: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); | ||||
|         break; | ||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|         break; | ||||
|     } | ||||
| #endif  // USE_ESP32 | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } | ||||
| void ADCSensor::update() { | ||||
|   float value_v = this->sample(); | ||||
|   ESP_LOGD(TAG, "'%s': Got voltage=%.2fV", this->get_name().c_str(), value_v); | ||||
|   ESP_LOGD(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); | ||||
|   this->publish_state(value_v); | ||||
| } | ||||
| float ADCSensor::sample() { | ||||
| #ifdef USE_ESP32 | ||||
|   int raw = adc1_get_raw(gpio_to_adc1(pin_->get_pin())); | ||||
|   float value_v = raw / 4095.0f; | ||||
| #if CONFIG_IDF_TARGET_ESP32 | ||||
|   switch (this->attenuation_) { | ||||
|     case ADC_ATTEN_DB_0: | ||||
|       value_v *= 1.1; | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_2_5: | ||||
|       value_v *= 1.5; | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_6: | ||||
|       value_v *= 2.2; | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_11: | ||||
|       value_v *= 3.9; | ||||
|       break; | ||||
|     default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|       break; | ||||
|   } | ||||
| #elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 | ||||
|   switch (this->attenuation_) { | ||||
|     case ADC_ATTEN_DB_0: | ||||
|       value_v *= 0.84; | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_2_5: | ||||
|       value_v *= 1.13; | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_6: | ||||
|       value_v *= 1.56; | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_11: | ||||
|       value_v *= 3.0; | ||||
|       break; | ||||
|     default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|       break; | ||||
|   } | ||||
| #endif | ||||
|   return value_v; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| float ADCSensor::sample() { | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|   return ESP.getVcc() / 1024.0f;  // NOLINT(readability-static-accessed-through-instance) | ||||
|   int raw = ESP.getVcc();  // NOLINT(readability-static-accessed-through-instance) | ||||
| #else | ||||
|   return analogRead(this->pin_->get_pin()) / 1024.0f;  // NOLINT | ||||
| #endif | ||||
|   int raw = analogRead(this->pin_->get_pin());  // NOLINT | ||||
| #endif | ||||
|   if (output_raw_) { | ||||
|     return raw; | ||||
|   } | ||||
|   return raw / 1024.0f; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| float ADCSensor::sample() { | ||||
|   if (!autorange_) { | ||||
|     int raw = adc1_get_raw(channel_); | ||||
|     if (raw == -1) { | ||||
|       return NAN; | ||||
|     } | ||||
|     if (output_raw_) { | ||||
|       return raw; | ||||
|     } | ||||
|     uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int) attenuation_]); | ||||
|     return mv / 1000.0f; | ||||
|   } | ||||
|  | ||||
|   int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095; | ||||
|   adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); | ||||
|   raw11 = adc1_get_raw(channel_); | ||||
|   if (raw11 < 4095) { | ||||
|     adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); | ||||
|     raw6 = adc1_get_raw(channel_); | ||||
|     if (raw6 < 4095) { | ||||
|       adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); | ||||
|       raw2 = adc1_get_raw(channel_); | ||||
|       if (raw2 < 4095) { | ||||
|         adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); | ||||
|         raw0 = adc1_get_raw(channel_); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (raw0 == -1 || raw2 == -1 || raw6 == -1 || raw11 == -1) { | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int) ADC_ATTEN_DB_11]); | ||||
|   uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int) ADC_ATTEN_DB_6]); | ||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]); | ||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]); | ||||
|  | ||||
|   // Contribution of each value, in range 0-2048 | ||||
|   uint32_t c11 = std::min(raw11, 2048); | ||||
|   uint32_t c6 = 2048 - std::abs(raw6 - 2048); | ||||
|   uint32_t c2 = 2048 - std::abs(raw2 - 2048); | ||||
|   uint32_t c0 = std::min(4095 - raw0, 2048); | ||||
|   // max theoretical csum value is 2048*4 = 8192 | ||||
|   uint32_t csum = c11 + c6 + c2 + c0; | ||||
|  | ||||
|   // each mv is max 3900; so max value is 3900*2048*4, fits in unsigned | ||||
|   uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||
|   return mv_scaled / (float) (csum * 1000U); | ||||
| } | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } | ||||
| #endif | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #include "driver/adc.h" | ||||
| #include <esp_adc_cal.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -17,7 +18,9 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|  public: | ||||
| #ifdef USE_ESP32 | ||||
|   /// Set the attenuation for this pin. Only available on the ESP32. | ||||
|   void set_attenuation(adc_atten_t attenuation); | ||||
|   void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; } | ||||
|   void set_channel(adc1_channel_t channel) { channel_ = channel; } | ||||
|   void set_autorange(bool autorange) { autorange_ = autorange; } | ||||
| #endif | ||||
|  | ||||
|   /// Update adc values. | ||||
| @@ -28,6 +31,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|   /// `HARDWARE_LATE` setup priority. | ||||
|   float get_setup_priority() const override; | ||||
|   void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } | ||||
|   void set_output_raw(bool output_raw) { output_raw_ = output_raw; } | ||||
|   float sample() override; | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| @@ -36,9 +40,13 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|  | ||||
|  protected: | ||||
|   InternalGPIOPin *pin_; | ||||
|   bool output_raw_{false}; | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||
|   adc1_channel_t channel_{}; | ||||
|   bool autorange_{false}; | ||||
|   esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {}; | ||||
| #endif | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -4,14 +4,24 @@ from esphome import pins | ||||
| from esphome.components import sensor, voltage_sampler | ||||
| from esphome.const import ( | ||||
|     CONF_ATTENUATION, | ||||
|     CONF_RAW, | ||||
|     CONF_ID, | ||||
|     CONF_INPUT, | ||||
|     CONF_NUMBER, | ||||
|     CONF_PIN, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32H2, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ) | ||||
|  | ||||
|  | ||||
| AUTO_LOAD = ["voltage_sampler"] | ||||
| @@ -21,6 +31,62 @@ ATTENUATION_MODES = { | ||||
|     "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, | ||||
|     "6db": cg.global_ns.ADC_ATTEN_DB_6, | ||||
|     "11db": cg.global_ns.ADC_ATTEN_DB_11, | ||||
|     "auto": "auto", | ||||
| } | ||||
|  | ||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | ||||
|  | ||||
| # From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h | ||||
| # pin to adc1 channel mapping | ||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
|     VARIANT_ESP32: { | ||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|     }, | ||||
|     VARIANT_ESP32S2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32S3: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32C3: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
|     VARIANT_ESP32H2: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -29,15 +95,16 @@ def validate_adc_pin(value): | ||||
|         return cv.only_on_esp8266("VCC") | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         from esphome.components.esp32 import is_esp32c3 | ||||
|  | ||||
|         value = pins.internal_gpio_input_pin_number(value) | ||||
|         if is_esp32c3(): | ||||
|             if not (0 <= value <= 4):  # ADC1 | ||||
|                 raise cv.Invalid("ESP32-C3: Only pins 0 though 4 support ADC.") | ||||
|         elif not (32 <= value <= 39):  # ADC1 | ||||
|             raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") | ||||
|     elif CORE.is_esp8266: | ||||
|         variant = get_esp32_variant() | ||||
|         if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: | ||||
|             raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") | ||||
|  | ||||
|         if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: | ||||
|             raise cv.Invalid(f"{variant} doesn't support ADC on this pin") | ||||
|         return pins.internal_gpio_input_pin_schema(value) | ||||
|  | ||||
|     if CORE.is_esp8266: | ||||
|         from esphome.components.esp8266.gpio import CONF_ANALOG | ||||
|  | ||||
|         value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( | ||||
| @@ -49,10 +116,14 @@ def validate_adc_pin(value): | ||||
|         return pins.gpio_pin_schema( | ||||
|             {CONF_ANALOG: True, CONF_INPUT: True}, internal=True | ||||
|         )(value) | ||||
|     else: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     return pins.internal_gpio_input_pin_schema(value) | ||||
|     raise NotImplementedError | ||||
|  | ||||
|  | ||||
| def validate_config(config): | ||||
|     if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": | ||||
|         raise cv.Invalid("Automatic attenuation cannot be used when raw output is set.") | ||||
|     return config | ||||
|  | ||||
|  | ||||
| adc_ns = cg.esphome_ns.namespace("adc") | ||||
| @@ -60,7 +131,7 @@ ADCSensor = adc_ns.class_( | ||||
|     "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=2, | ||||
| @@ -71,12 +142,14 @@ CONFIG_SCHEMA = ( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(ADCSensor), | ||||
|             cv.Required(CONF_PIN): validate_adc_pin, | ||||
|             cv.Optional(CONF_RAW, default=False): cv.boolean, | ||||
|             cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( | ||||
|                 cv.only_on_esp32, cv.enum(ATTENUATION_MODES, lower=True) | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(cv.polling_component_schema("60s")), | ||||
|     validate_config, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -91,5 +164,17 @@ async def to_code(config): | ||||
|         pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||
|         cg.add(var.set_pin(pin)) | ||||
|  | ||||
|     if CONF_RAW in config: | ||||
|         cg.add(var.set_output_raw(config[CONF_RAW])) | ||||
|  | ||||
|     if CONF_ATTENUATION in config: | ||||
|         cg.add(var.set_attenuation(config[CONF_ATTENUATION])) | ||||
|         if config[CONF_ATTENUATION] == "auto": | ||||
|             cg.add(var.set_autorange(cg.global_ns.true)) | ||||
|         else: | ||||
|             cg.add(var.set_attenuation(config[CONF_ATTENUATION])) | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         variant = get_esp32_variant() | ||||
|         pin_num = config[CONF_PIN][CONF_NUMBER] | ||||
|         chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] | ||||
|         cg.add(var.set_channel(chan)) | ||||
|   | ||||
| @@ -73,7 +73,7 @@ void AHT10Component::update() { | ||||
|   bool success = false; | ||||
|   for (int i = 0; i < AHT10_ATTEMPTS; ++i) { | ||||
|     ESP_LOGVV(TAG, "Attempt %d at %6u", i, millis()); | ||||
|     delay_microseconds_accurate(4); | ||||
|     delayMicroseconds(4); | ||||
|  | ||||
|     uint8_t reg = 0; | ||||
|     if (this->write(®, 1) != i2c::ERROR_OK) { | ||||
|   | ||||
| @@ -4,7 +4,8 @@ from esphome.components import sensor, ble_client | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_BATTERY_LEVEL, | ||||
|     ICON_BATTERY, | ||||
|     DEVICE_CLASS_BATTERY, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     CONF_ILLUMINANCE, | ||||
|     ICON_BRIGHTNESS_5, | ||||
|     UNIT_PERCENT, | ||||
| @@ -20,10 +21,15 @@ CONFIG_SCHEMA = ( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(Am43), | ||||
|             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( | ||||
|                 UNIT_PERCENT, ICON_BATTERY, 0 | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 device_class=DEVICE_CLASS_BATTERY, | ||||
|                 accuracy_decimals=0, | ||||
|                 entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             ), | ||||
|             cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( | ||||
|                 UNIT_PERCENT, ICON_BRIGHTNESS_5, 0 | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 icon=ICON_BRIGHTNESS_5, | ||||
|                 accuracy_decimals=0, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|   | ||||
| @@ -103,21 +103,21 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) { | ||||
|       break; | ||||
|     } | ||||
|     case READ_TARGET_TEMPERATURE: { | ||||
|       this->target_temp_ = strtof(this->buf_, nullptr); | ||||
|       this->target_temp_ = parse_number<float>(this->buf_, sizeof(this->buf_)).value_or(0.0f); | ||||
|       if (this->fahrenheit_) | ||||
|         this->target_temp_ = ftoc(this->target_temp_); | ||||
|       this->has_target_temp_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case SET_TARGET_TEMPERATURE: { | ||||
|       this->target_temp_ = strtof(this->buf_, nullptr); | ||||
|       this->target_temp_ = parse_number<float>(this->buf_, sizeof(this->buf_)).value_or(0.0f); | ||||
|       if (this->fahrenheit_) | ||||
|         this->target_temp_ = ftoc(this->target_temp_); | ||||
|       this->has_target_temp_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case READ_CURRENT_TEMPERATURE: { | ||||
|       this->current_temp_ = strtof(this->buf_, nullptr); | ||||
|       this->current_temp_ = parse_number<float>(this->buf_, sizeof(this->buf_)).value_or(0.0f); | ||||
|       if (this->fahrenheit_) | ||||
|         this->current_temp_ = ftoc(this->current_temp_); | ||||
|       this->has_current_temp_ = true; | ||||
|   | ||||
| @@ -182,6 +182,8 @@ message DeviceInfoResponse { | ||||
|   // The esphome project details if set | ||||
|   string project_name = 8; | ||||
|   string project_version = 9; | ||||
|  | ||||
|   uint32 webserver_port = 10; | ||||
| } | ||||
|  | ||||
| message ListEntitiesRequest { | ||||
| @@ -201,6 +203,14 @@ message SubscribeStatesRequest { | ||||
|   // Empty | ||||
| } | ||||
|  | ||||
| // ==================== COMMON ===================== | ||||
|  | ||||
| enum EntityCategory { | ||||
|   ENTITY_CATEGORY_NONE = 0; | ||||
|   ENTITY_CATEGORY_CONFIG = 1; | ||||
|   ENTITY_CATEGORY_DIAGNOSTIC = 2; | ||||
| } | ||||
|  | ||||
| // ==================== BINARY SENSOR ==================== | ||||
| message ListEntitiesBinarySensorResponse { | ||||
|   option (id) = 12; | ||||
| @@ -216,6 +226,7 @@ message ListEntitiesBinarySensorResponse { | ||||
|   bool is_status_binary_sensor = 6; | ||||
|   bool disabled_by_default = 7; | ||||
|   string icon = 8; | ||||
|   EntityCategory entity_category = 9; | ||||
| } | ||||
| message BinarySensorStateResponse { | ||||
|   option (id) = 21; | ||||
| @@ -247,6 +258,7 @@ message ListEntitiesCoverResponse { | ||||
|   string device_class = 8; | ||||
|   bool disabled_by_default = 9; | ||||
|   string icon = 10; | ||||
|   EntityCategory entity_category = 11; | ||||
| } | ||||
|  | ||||
| enum LegacyCoverState { | ||||
| @@ -316,6 +328,7 @@ message ListEntitiesFanResponse { | ||||
|   int32 supported_speed_count = 8; | ||||
|   bool disabled_by_default = 9; | ||||
|   string icon = 10; | ||||
|   EntityCategory entity_category = 11; | ||||
| } | ||||
| enum FanSpeed { | ||||
|   FAN_SPEED_LOW = 0; | ||||
| @@ -392,6 +405,7 @@ message ListEntitiesLightResponse { | ||||
|   repeated string effects = 11; | ||||
|   bool disabled_by_default = 13; | ||||
|   string icon = 14; | ||||
|   EntityCategory entity_category = 15; | ||||
| } | ||||
| message LightStateResponse { | ||||
|   option (id) = 24; | ||||
| @@ -480,6 +494,7 @@ message ListEntitiesSensorResponse { | ||||
|   // Last reset type removed in 2021.9.0 | ||||
|   SensorLastResetType legacy_last_reset_type = 11; | ||||
|   bool disabled_by_default = 12; | ||||
|   EntityCategory entity_category = 13; | ||||
| } | ||||
| message SensorStateResponse { | ||||
|   option (id) = 25; | ||||
| @@ -508,6 +523,7 @@ message ListEntitiesSwitchResponse { | ||||
|   string icon = 5; | ||||
|   bool assumed_state = 6; | ||||
|   bool disabled_by_default = 7; | ||||
|   EntityCategory entity_category = 8; | ||||
| } | ||||
| message SwitchStateResponse { | ||||
|   option (id) = 26; | ||||
| @@ -541,6 +557,7 @@ message ListEntitiesTextSensorResponse { | ||||
|  | ||||
|   string icon = 5; | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
| } | ||||
| message TextSensorStateResponse { | ||||
|   option (id) = 27; | ||||
| @@ -701,6 +718,8 @@ message ListEntitiesCameraResponse { | ||||
|   string name = 3; | ||||
|   string unique_id = 4; | ||||
|   bool disabled_by_default = 5; | ||||
|   string icon = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
| } | ||||
|  | ||||
| message CameraImageResponse { | ||||
| @@ -795,6 +814,7 @@ message ListEntitiesClimateResponse { | ||||
|   repeated string supported_custom_presets = 17; | ||||
|   bool disabled_by_default = 18; | ||||
|   string icon = 19; | ||||
|   EntityCategory entity_category = 20; | ||||
| } | ||||
| message ClimateStateResponse { | ||||
|   option (id) = 47; | ||||
| @@ -863,6 +883,7 @@ message ListEntitiesNumberResponse { | ||||
|   float max_value = 7; | ||||
|   float step = 8; | ||||
|   bool disabled_by_default = 9; | ||||
|   EntityCategory entity_category = 10; | ||||
| } | ||||
| message NumberStateResponse { | ||||
|   option (id) = 50; | ||||
| @@ -900,6 +921,7 @@ message ListEntitiesSelectResponse { | ||||
|   string icon = 5; | ||||
|   repeated string options = 6; | ||||
|   bool disabled_by_default = 7; | ||||
|   EntityCategory entity_category = 8; | ||||
| } | ||||
| message SelectStateResponse { | ||||
|   option (id) = 53; | ||||
|   | ||||
| @@ -184,6 +184,7 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_ | ||||
|   msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); | ||||
|   msg.disabled_by_default = binary_sensor->is_disabled_by_default(); | ||||
|   msg.icon = binary_sensor->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(binary_sensor->get_entity_category()); | ||||
|   return this->send_list_entities_binary_sensor_response(msg); | ||||
| } | ||||
| #endif | ||||
| @@ -217,6 +218,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) { | ||||
|   msg.device_class = cover->get_device_class(); | ||||
|   msg.disabled_by_default = cover->is_disabled_by_default(); | ||||
|   msg.icon = cover->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(cover->get_entity_category()); | ||||
|   return this->send_list_entities_cover_response(msg); | ||||
| } | ||||
| void APIConnection::cover_command(const CoverCommandRequest &msg) { | ||||
| @@ -283,6 +285,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { | ||||
|   msg.supported_speed_count = traits.supported_speed_count(); | ||||
|   msg.disabled_by_default = fan->is_disabled_by_default(); | ||||
|   msg.icon = fan->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category()); | ||||
|   return this->send_list_entities_fan_response(msg); | ||||
| } | ||||
| void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
| @@ -346,6 +349,7 @@ bool APIConnection::send_light_info(light::LightState *light) { | ||||
|  | ||||
|   msg.disabled_by_default = light->is_disabled_by_default(); | ||||
|   msg.icon = light->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(light->get_entity_category()); | ||||
|  | ||||
|   for (auto mode : traits.get_supported_color_modes()) | ||||
|     msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode)); | ||||
| @@ -432,7 +436,7 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { | ||||
|   msg.device_class = sensor->get_device_class(); | ||||
|   msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); | ||||
|   msg.disabled_by_default = sensor->is_disabled_by_default(); | ||||
|  | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(sensor->get_entity_category()); | ||||
|   return this->send_list_entities_sensor_response(msg); | ||||
| } | ||||
| #endif | ||||
| @@ -456,6 +460,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) { | ||||
|   msg.icon = a_switch->get_icon(); | ||||
|   msg.assumed_state = a_switch->assumed_state(); | ||||
|   msg.disabled_by_default = a_switch->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(a_switch->get_entity_category()); | ||||
|   return this->send_list_entities_switch_response(msg); | ||||
| } | ||||
| void APIConnection::switch_command(const SwitchCommandRequest &msg) { | ||||
| @@ -491,6 +496,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) | ||||
|     msg.unique_id = get_default_unique_id("text_sensor", text_sensor); | ||||
|   msg.icon = text_sensor->get_icon(); | ||||
|   msg.disabled_by_default = text_sensor->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(text_sensor->get_entity_category()); | ||||
|   return this->send_list_entities_text_sensor_response(msg); | ||||
| } | ||||
| #endif | ||||
| @@ -537,6 +543,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { | ||||
|  | ||||
|   msg.disabled_by_default = climate->is_disabled_by_default(); | ||||
|   msg.icon = climate->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category()); | ||||
|  | ||||
|   msg.supports_current_temperature = traits.get_supports_current_temperature(); | ||||
|   msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); | ||||
| @@ -611,6 +618,7 @@ bool APIConnection::send_number_info(number::Number *number) { | ||||
|   msg.unique_id = get_default_unique_id("number", number); | ||||
|   msg.icon = number->get_icon(); | ||||
|   msg.disabled_by_default = number->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(number->get_entity_category()); | ||||
|  | ||||
|   msg.min_value = number->traits.get_min_value(); | ||||
|   msg.max_value = number->traits.get_max_value(); | ||||
| @@ -648,6 +656,7 @@ bool APIConnection::send_select_info(select::Select *select) { | ||||
|   msg.unique_id = get_default_unique_id("select", select); | ||||
|   msg.icon = select->get_icon(); | ||||
|   msg.disabled_by_default = select->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(select->get_entity_category()); | ||||
|  | ||||
|   for (const auto &option : select->traits.get_options()) | ||||
|     msg.options.push_back(option); | ||||
| @@ -680,6 +689,8 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { | ||||
|   msg.name = camera->get_name(); | ||||
|   msg.unique_id = get_default_unique_id("camera", camera); | ||||
|   msg.disabled_by_default = camera->is_disabled_by_default(); | ||||
|   msg.icon = camera->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(camera->get_entity_category()); | ||||
|   return this->send_list_entities_camera_response(msg); | ||||
| } | ||||
| void APIConnection::camera_image(const CameraImageRequest &msg) { | ||||
| @@ -758,6 +769,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
| #ifdef ESPHOME_PROJECT_NAME | ||||
|   resp.project_name = ESPHOME_PROJECT_NAME; | ||||
|   resp.project_version = ESPHOME_PROJECT_VERSION; | ||||
| #endif | ||||
| #ifdef USE_WEBSERVER | ||||
|   resp.webserver_port = WEBSERVER_PORT; | ||||
| #endif | ||||
|   return resp; | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,18 @@ | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) { | ||||
|   switch (value) { | ||||
|     case enums::ENTITY_CATEGORY_NONE: | ||||
|       return "ENTITY_CATEGORY_NONE"; | ||||
|     case enums::ENTITY_CATEGORY_CONFIG: | ||||
|       return "ENTITY_CATEGORY_CONFIG"; | ||||
|     case enums::ENTITY_CATEGORY_DIAGNOSTIC: | ||||
|       return "ENTITY_CATEGORY_DIAGNOSTIC"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| template<> const char *proto_enum_to_string<enums::LegacyCoverState>(enums::LegacyCoverState value) { | ||||
|   switch (value) { | ||||
|     case enums::LEGACY_COVER_STATE_OPEN: | ||||
| @@ -396,6 +408,10 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|       this->has_deep_sleep = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 10: { | ||||
|       this->webserver_port = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -444,6 +460,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(7, this->has_deep_sleep); | ||||
|   buffer.encode_string(8, this->project_name); | ||||
|   buffer.encode_string(9, this->project_version); | ||||
|   buffer.encode_uint32(10, this->webserver_port); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
| @@ -484,6 +501,11 @@ void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
|   out.append("  project_version: "); | ||||
|   out.append("'").append(this->project_version).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  webserver_port: "); | ||||
|   sprintf(buffer, "%u", this->webserver_port); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -509,6 +531,10 @@ bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVar | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 9: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -558,6 +584,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(6, this->is_status_binary_sensor); | ||||
|   buffer.encode_bool(7, this->disabled_by_default); | ||||
|   buffer.encode_string(8, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(9, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { | ||||
| @@ -595,6 +622,10 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { | ||||
|   out.append("  icon: "); | ||||
|   out.append("'").append(this->icon).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -664,6 +695,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 11: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -715,6 +750,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(8, this->device_class); | ||||
|   buffer.encode_bool(9, this->disabled_by_default); | ||||
|   buffer.encode_string(10, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(11, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesCoverResponse::dump_to(std::string &out) const { | ||||
| @@ -760,6 +796,10 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { | ||||
|   out.append("  icon: "); | ||||
|   out.append("'").append(this->icon).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -948,6 +988,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 11: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -995,6 +1039,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_int32(8, this->supported_speed_count); | ||||
|   buffer.encode_bool(9, this->disabled_by_default); | ||||
|   buffer.encode_string(10, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(11, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesFanResponse::dump_to(std::string &out) const { | ||||
| @@ -1041,6 +1086,10 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { | ||||
|   out.append("  icon: "); | ||||
|   out.append("'").append(this->icon).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -1267,6 +1316,10 @@ bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt val | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 15: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -1334,6 +1387,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   } | ||||
|   buffer.encode_bool(13, this->disabled_by_default); | ||||
|   buffer.encode_string(14, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(15, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesLightResponse::dump_to(std::string &out) const { | ||||
| @@ -1401,6 +1455,10 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { | ||||
|   out.append("  icon: "); | ||||
|   out.append("'").append(this->icon).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -1860,6 +1918,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 13: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -1917,6 +1979,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_enum<enums::SensorStateClass>(10, this->state_class); | ||||
|   buffer.encode_enum<enums::SensorLastResetType>(11, this->legacy_last_reset_type); | ||||
|   buffer.encode_bool(12, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(13, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesSensorResponse::dump_to(std::string &out) const { | ||||
| @@ -1971,6 +2034,10 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { | ||||
|   out.append("  disabled_by_default: "); | ||||
|   out.append(YESNO(this->disabled_by_default)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -2033,6 +2100,10 @@ bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -2077,6 +2148,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(5, this->icon); | ||||
|   buffer.encode_bool(6, this->assumed_state); | ||||
|   buffer.encode_bool(7, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(8, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesSwitchResponse::dump_to(std::string &out) const { | ||||
| @@ -2110,6 +2182,10 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { | ||||
|   out.append("  disabled_by_default: "); | ||||
|   out.append(YESNO(this->disabled_by_default)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -2197,6 +2273,10 @@ bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarIn | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 7: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -2240,6 +2320,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(4, this->unique_id); | ||||
|   buffer.encode_string(5, this->icon); | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { | ||||
| @@ -2269,6 +2350,10 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { | ||||
|   out.append("  disabled_by_default: "); | ||||
|   out.append(YESNO(this->disabled_by_default)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -2892,6 +2977,10 @@ bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 7: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -2910,6 +2999,10 @@ bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDel | ||||
|       this->unique_id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 6: { | ||||
|       this->icon = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -2930,6 +3023,8 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(3, this->name); | ||||
|   buffer.encode_string(4, this->unique_id); | ||||
|   buffer.encode_bool(5, this->disabled_by_default); | ||||
|   buffer.encode_string(6, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesCameraResponse::dump_to(std::string &out) const { | ||||
| @@ -2955,6 +3050,14 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { | ||||
|   out.append("  disabled_by_default: "); | ||||
|   out.append(YESNO(this->disabled_by_default)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  icon: "); | ||||
|   out.append("'").append(this->icon).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -3082,6 +3185,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 20: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -3170,6 +3277,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   } | ||||
|   buffer.encode_bool(18, this->disabled_by_default); | ||||
|   buffer.encode_string(19, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(20, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesClimateResponse::dump_to(std::string &out) const { | ||||
| @@ -3266,6 +3374,10 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { | ||||
|   out.append("  icon: "); | ||||
|   out.append("'").append(this->icon).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -3642,6 +3754,10 @@ bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 10: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -3700,6 +3816,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_float(7, this->max_value); | ||||
|   buffer.encode_float(8, this->step); | ||||
|   buffer.encode_bool(9, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(10, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesNumberResponse::dump_to(std::string &out) const { | ||||
| @@ -3744,6 +3861,10 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { | ||||
|   out.append("  disabled_by_default: "); | ||||
|   out.append(YESNO(this->disabled_by_default)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -3836,6 +3957,10 @@ bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt va | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -3886,6 +4011,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|     buffer.encode_string(6, it, true); | ||||
|   } | ||||
|   buffer.encode_bool(7, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(8, this->entity_category); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesSelectResponse::dump_to(std::string &out) const { | ||||
| @@ -3921,6 +4047,10 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { | ||||
|   out.append("  disabled_by_default: "); | ||||
|   out.append(YESNO(this->disabled_by_default)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
|   | ||||
| @@ -9,6 +9,11 @@ namespace api { | ||||
|  | ||||
| namespace enums { | ||||
|  | ||||
| enum EntityCategory : uint32_t { | ||||
|   ENTITY_CATEGORY_NONE = 0, | ||||
|   ENTITY_CATEGORY_CONFIG = 1, | ||||
|   ENTITY_CATEGORY_DIAGNOSTIC = 2, | ||||
| }; | ||||
| enum LegacyCoverState : uint32_t { | ||||
|   LEGACY_COVER_STATE_OPEN = 0, | ||||
|   LEGACY_COVER_STATE_CLOSED = 1, | ||||
| @@ -224,6 +229,7 @@ class DeviceInfoResponse : public ProtoMessage { | ||||
|   bool has_deep_sleep{false}; | ||||
|   std::string project_name{}; | ||||
|   std::string project_version{}; | ||||
|   uint32_t webserver_port{0}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -270,6 +276,7 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { | ||||
|   bool is_status_binary_sensor{false}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -306,6 +313,7 @@ class ListEntitiesCoverResponse : public ProtoMessage { | ||||
|   std::string device_class{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -363,6 +371,7 @@ class ListEntitiesFanResponse : public ProtoMessage { | ||||
|   int32_t supported_speed_count{0}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -428,6 +437,7 @@ class ListEntitiesLightResponse : public ProtoMessage { | ||||
|   std::vector<std::string> effects{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -516,6 +526,7 @@ class ListEntitiesSensorResponse : public ProtoMessage { | ||||
|   enums::SensorStateClass state_class{}; | ||||
|   enums::SensorLastResetType legacy_last_reset_type{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -549,6 +560,7 @@ class ListEntitiesSwitchResponse : public ProtoMessage { | ||||
|   std::string icon{}; | ||||
|   bool assumed_state{false}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -593,6 +605,7 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -803,6 +816,8 @@ class ListEntitiesCameraResponse : public ProtoMessage { | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -861,6 +876,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { | ||||
|   std::vector<std::string> supported_custom_presets{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -940,6 +956,7 @@ class ListEntitiesNumberResponse : public ProtoMessage { | ||||
|   float max_value{0.0f}; | ||||
|   float step{0.0f}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -985,6 +1002,7 @@ class ListEntitiesSelectResponse : public ProtoMessage { | ||||
|   std::string icon{}; | ||||
|   std::vector<std::string> options{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
|   | ||||
| @@ -77,7 +77,7 @@ void APIServer::setup() { | ||||
|   this->last_connected_ = millis(); | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
|   if (esp32_camera::global_esp32_camera != nullptr) { | ||||
|   if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { | ||||
|     esp32_camera::global_esp32_camera->add_image_callback( | ||||
|         [this](const std::shared_ptr<esp32_camera::CameraImage> &image) { | ||||
|           for (auto &c : this->clients_) | ||||
|   | ||||
| @@ -23,7 +23,6 @@ async def async_run_logs(config, address): | ||||
|     _LOGGER.info("Starting log output from %s using esphome API", address) | ||||
|     zc = zeroconf.Zeroconf() | ||||
|     cli = APIClient( | ||||
|         asyncio.get_event_loop(), | ||||
|         address, | ||||
|         port, | ||||
|         password, | ||||
|   | ||||
| @@ -8,6 +8,18 @@ | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { | ||||
|  public: | ||||
|   TemplatableStringValue() : TemplatableValue<std::string, X...>() {} | ||||
|  | ||||
|   template<typename F, enable_if_t<!is_callable<F, X...>::value, int> = 0> | ||||
|   TemplatableStringValue(F value) : TemplatableValue<std::string, X...>(value) {} | ||||
|  | ||||
|   template<typename F, enable_if_t<is_callable<F, X...>::value, int> = 0> | ||||
|   TemplatableStringValue(F f) | ||||
|       : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {} | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class TemplatableKeyValuePair { | ||||
|  public: | ||||
|   template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} | ||||
| @@ -19,7 +31,8 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|  public: | ||||
|   explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {} | ||||
|  | ||||
|   TEMPLATABLE_STRING_VALUE(service); | ||||
|   template<typename T> void set_service(T service) { this->service_ = service; } | ||||
|  | ||||
|   template<typename T> void add_data(std::string key, T value) { | ||||
|     this->data_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); | ||||
|   } | ||||
| @@ -58,6 +71,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|  protected: | ||||
|   APIServer *parent_; | ||||
|   bool is_event_; | ||||
|   TemplatableStringValue<Ts...> service_{}; | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> data_; | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> data_template_; | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> variables_; | ||||
|   | ||||
| @@ -12,6 +12,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_PERCENT, | ||||
| @@ -49,12 +50,14 @@ CONFIG_SCHEMA = ( | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_BATTERY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             ), | ||||
|             cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_VOLT, | ||||
|                 accuracy_decimals=3, | ||||
|                 device_class=DEVICE_CLASS_VOLTAGE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|   | ||||
| @@ -17,6 +17,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_POWER_FACTOR, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     ICON_LIGHTBULB, | ||||
|     ICON_CURRENT_AC, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
| @@ -125,6 +126,7 @@ CONFIG_SCHEMA = ( | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             ), | ||||
|             cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), | ||||
|             cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( | ||||
|   | ||||
| @@ -13,6 +13,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_LUX, | ||||
| @@ -51,6 +52,7 @@ CONFIG_SCHEMA = ( | ||||
|                 accuracy_decimals=3, | ||||
|                 device_class=DEVICE_CLASS_VOLTAGE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             ), | ||||
|             cv.Optional(CONF_MOISTURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|   | ||||
| @@ -79,6 +79,9 @@ void BH1750Sensor::read_data_() { | ||||
|  | ||||
|   float lx = float(raw_value) / 1.2f; | ||||
|   lx *= 69.0f / this->measurement_duration_; | ||||
|   if (this->resolution_ == BH1750_RESOLUTION_0P5_LX) { | ||||
|     lx /= 2.0f; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx); | ||||
|   this->publish_state(lx); | ||||
|   this->status_clear_warning(); | ||||
|   | ||||
| @@ -44,10 +44,11 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_PRESENCE, | ||||
|     DEVICE_CLASS_PROBLEM, | ||||
|     DEVICE_CLASS_RUNNING, | ||||
|     DEVICE_CLASS_SAFETY, | ||||
|     DEVICE_CLASS_SMOKE, | ||||
|     DEVICE_CLASS_SOUND, | ||||
|     DEVICE_CLASS_UPDATE, | ||||
|     DEVICE_CLASS_TAMPER, | ||||
|     DEVICE_CLASS_VIBRATION, | ||||
|     DEVICE_CLASS_WINDOW, | ||||
| ) | ||||
| @@ -76,10 +77,11 @@ DEVICE_CLASSES = [ | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_PRESENCE, | ||||
|     DEVICE_CLASS_PROBLEM, | ||||
|     DEVICE_CLASS_RUNNING, | ||||
|     DEVICE_CLASS_SAFETY, | ||||
|     DEVICE_CLASS_SMOKE, | ||||
|     DEVICE_CLASS_SOUND, | ||||
|     DEVICE_CLASS_UPDATE, | ||||
|     DEVICE_CLASS_TAMPER, | ||||
|     DEVICE_CLASS_VIBRATION, | ||||
|     DEVICE_CLASS_WINDOW, | ||||
| ] | ||||
| @@ -313,6 +315,7 @@ def validate_multi_click_timing(value): | ||||
|  | ||||
| device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") | ||||
|  | ||||
|  | ||||
| BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(BinarySensor), | ||||
|   | ||||
| @@ -67,7 +67,7 @@ async def to_code(config): | ||||
|             var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) | ||||
|         ) | ||||
|     elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): | ||||
|         uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) | ||||
|         uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) | ||||
|         cg.add(var.set_service_uuid128(uuid128)) | ||||
|  | ||||
|     if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): | ||||
| @@ -87,7 +87,9 @@ async def to_code(config): | ||||
|     elif len(config[CONF_CHARACTERISTIC_UUID]) == len( | ||||
|         esp32_ble_tracker.bt_uuid128_format | ||||
|     ): | ||||
|         uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_CHARACTERISTIC_UUID]) | ||||
|         uuid128 = esp32_ble_tracker.as_reversed_hex_array( | ||||
|             config[CONF_CHARACTERISTIC_UUID] | ||||
|         ) | ||||
|         cg.add(var.set_char_uuid128(uuid128)) | ||||
|  | ||||
|     if CONF_DESCRIPTOR_UUID in config: | ||||
| @@ -108,7 +110,9 @@ async def to_code(config): | ||||
|         elif len(config[CONF_DESCRIPTOR_UUID]) == len( | ||||
|             esp32_ble_tracker.bt_uuid128_format | ||||
|         ): | ||||
|             uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_DESCRIPTOR_UUID]) | ||||
|             uuid128 = esp32_ble_tracker.as_reversed_hex_array( | ||||
|                 config[CONF_DESCRIPTOR_UUID] | ||||
|             ) | ||||
|             cg.add(var.set_descr_uuid128(uuid128)) | ||||
|  | ||||
|     if CONF_LAMBDA in config: | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c | ||||
| from esphome.const import CONF_ID | ||||
| from esphome.core import CORE | ||||
|  | ||||
| CODEOWNERS = ["@trvrnrth"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
| @@ -62,9 +61,8 @@ async def to_code(config): | ||||
|         var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) | ||||
|     ) | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         # Although this component does not use SPI, the BSEC library requires the SPI library | ||||
|         cg.add_library("SPI", None) | ||||
|     # Although this component does not use SPI, the BSEC library requires the SPI library | ||||
|     cg.add_library("SPI", None) | ||||
|  | ||||
|     cg.add_define("USE_BSEC") | ||||
|     cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") | ||||
|   | ||||
							
								
								
									
										45
									
								
								esphome/components/cap1188/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								esphome/components/cap1188/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c | ||||
| from esphome.const import CONF_ID, CONF_RESET_PIN | ||||
| from esphome import pins | ||||
|  | ||||
| CONF_TOUCH_THRESHOLD = "touch_threshold" | ||||
| CONF_ALLOW_MULTIPLE_TOUCHES = "allow_multiple_touches" | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
| AUTO_LOAD = ["binary_sensor", "output"] | ||||
| CODEOWNERS = ["@MrEditor97"] | ||||
|  | ||||
| cap1188_ns = cg.esphome_ns.namespace("cap1188") | ||||
| CONF_CAP1188_ID = "cap1188_id" | ||||
| CAP1188Component = cap1188_ns.class_("CAP1188Component", cg.Component, i2c.I2CDevice) | ||||
|  | ||||
| MULTI_CONF = True | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(CAP1188Component), | ||||
|             cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Optional(CONF_TOUCH_THRESHOLD, default=0x20): cv.int_range( | ||||
|                 min=0x01, max=0x80 | ||||
|             ), | ||||
|             cv.Optional(CONF_ALLOW_MULTIPLE_TOUCHES, default=False): cv.boolean, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
|     .extend(i2c.i2c_device_schema(0x29)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD])) | ||||
|     cg.add(var.set_allow_multiple_touches(config[CONF_ALLOW_MULTIPLE_TOUCHES])) | ||||
|  | ||||
|     if CONF_RESET_PIN in config: | ||||
|         pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) | ||||
|         cg.add(var.set_reset_pin(pin)) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
							
								
								
									
										25
									
								
								esphome/components/cap1188/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								esphome/components/cap1188/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor | ||||
| from esphome.const import CONF_CHANNEL, CONF_ID | ||||
| from . import cap1188_ns, CAP1188Component, CONF_CAP1188_ID | ||||
|  | ||||
| DEPENDENCIES = ["cap1188"] | ||||
| CAP1188Channel = cap1188_ns.class_("CAP1188Channel", binary_sensor.BinarySensor) | ||||
|  | ||||
| CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(CAP1188Channel), | ||||
|         cv.GenerateID(CONF_CAP1188_ID): cv.use_id(CAP1188Component), | ||||
|         cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await binary_sensor.register_binary_sensor(var, config) | ||||
|     hub = await cg.get_variable(config[CONF_CAP1188_ID]) | ||||
|     cg.add(var.set_channel(config[CONF_CHANNEL])) | ||||
|  | ||||
|     cg.add(hub.register_channel(var)) | ||||
							
								
								
									
										88
									
								
								esphome/components/cap1188/cap1188.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								esphome/components/cap1188/cap1188.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| #include "cap1188.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace cap1188 { | ||||
|  | ||||
| static const char *const TAG = "cap1188"; | ||||
|  | ||||
| void CAP1188Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up CAP1188..."); | ||||
|  | ||||
|   // Reset device using the reset pin | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->setup(); | ||||
|     this->reset_pin_->digital_write(false); | ||||
|     delay(100);  // NOLINT | ||||
|     this->reset_pin_->digital_write(true); | ||||
|     delay(100);  // NOLINT | ||||
|     this->reset_pin_->digital_write(false); | ||||
|     delay(100);  // NOLINT | ||||
|   } | ||||
|  | ||||
|   // Check if CAP1188 is actually connected | ||||
|   this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_); | ||||
|   this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_); | ||||
|   this->read_byte(CAP1188_REVISION, &this->cap1188_revision_); | ||||
|  | ||||
|   if ((this->cap1188_product_id_ != 0x50) || (this->cap1188_manufacture_id_ != 0x5D)) { | ||||
|     this->error_code_ = COMMUNICATION_FAILED; | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Set sensitivity | ||||
|   uint8_t sensitivity = 0; | ||||
|   this->read_byte(CAP1188_SENSITVITY, &sensitivity); | ||||
|   sensitivity = sensitivity & 0x0f; | ||||
|   this->write_byte(CAP1188_SENSITVITY, sensitivity | this->touch_threshold_); | ||||
|  | ||||
|   // Allow multiple touches | ||||
|   this->write_byte(CAP1188_MULTI_TOUCH, this->allow_multiple_touches_); | ||||
|  | ||||
|   // Have LEDs follow touches | ||||
|   this->write_byte(CAP1188_LED_LINK, 0xFF); | ||||
|  | ||||
|   // Speed up a bit | ||||
|   this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30); | ||||
| } | ||||
|  | ||||
| void CAP1188Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "CAP1188:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   LOG_PIN("  Reset Pin: ", this->reset_pin_); | ||||
|   ESP_LOGCONFIG(TAG, "  Product ID: 0x%x", this->cap1188_product_id_); | ||||
|   ESP_LOGCONFIG(TAG, "  Manufacture ID: 0x%x", this->cap1188_manufacture_id_); | ||||
|   ESP_LOGCONFIG(TAG, "  Revision ID: 0x%x", this->cap1188_revision_); | ||||
|  | ||||
|   switch (this->error_code_) { | ||||
|     case COMMUNICATION_FAILED: | ||||
|       ESP_LOGE(TAG, "Product ID or Manufacture ID of the connected device does not match a known CAP1188."); | ||||
|       break; | ||||
|     case NONE: | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void CAP1188Component::loop() { | ||||
|   uint8_t touched = 0; | ||||
|  | ||||
|   this->read_register(CAP1188_SENSOR_INPUT_STATUS, &touched, 1); | ||||
|  | ||||
|   if (touched) { | ||||
|     uint8_t data = 0; | ||||
|     this->read_register(CAP1188_MAIN, &data, 1); | ||||
|     data = data & ~CAP1188_MAIN_INT; | ||||
|  | ||||
|     this->write_register(CAP1188_MAIN, &data, 2); | ||||
|   } | ||||
|  | ||||
|   for (auto *channel : this->channels_) { | ||||
|     channel->process(touched); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace cap1188 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										68
									
								
								esphome/components/cap1188/cap1188.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								esphome/components/cap1188/cap1188.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/output/binary_output.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace cap1188 { | ||||
|  | ||||
| enum { | ||||
|   CAP1188_I2CADDR = 0x29, | ||||
|   CAP1188_SENSOR_INPUT_STATUS = 0x3, | ||||
|   CAP1188_MULTI_TOUCH = 0x2A, | ||||
|   CAP1188_LED_LINK = 0x72, | ||||
|   CAP1188_PRODUCT_ID = 0xFD, | ||||
|   CAP1188_MANUFACTURE_ID = 0xFE, | ||||
|   CAP1188_STAND_BY_CONFIGURATION = 0x41, | ||||
|   CAP1188_REVISION = 0xFF, | ||||
|   CAP1188_MAIN = 0x00, | ||||
|   CAP1188_MAIN_INT = 0x01, | ||||
|   CAP1188_LEDPOL = 0x73, | ||||
|   CAP1188_INTERUPT_REPEAT = 0x28, | ||||
|   CAP1188_SENSITVITY = 0x1f, | ||||
| }; | ||||
|  | ||||
| class CAP1188Channel : public binary_sensor::BinarySensor { | ||||
|  public: | ||||
|   void set_channel(uint8_t channel) { channel_ = channel; } | ||||
|   void process(uint8_t data) { this->publish_state(static_cast<bool>(data & (1 << this->channel_))); } | ||||
|  | ||||
|  protected: | ||||
|   uint8_t channel_{0}; | ||||
| }; | ||||
|  | ||||
| class CAP1188Component : public Component, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void register_channel(CAP1188Channel *channel) { this->channels_.push_back(channel); } | ||||
|   void set_touch_threshold(uint8_t touch_threshold) { this->touch_threshold_ = touch_threshold; }; | ||||
|   void set_allow_multiple_touches(bool allow_multiple_touches) { | ||||
|     this->allow_multiple_touches_ = allow_multiple_touches ? 0x41 : 0x80; | ||||
|   }; | ||||
|   void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|   void loop() override; | ||||
|  | ||||
|  protected: | ||||
|   std::vector<CAP1188Channel *> channels_{}; | ||||
|   uint8_t touch_threshold_{0x20}; | ||||
|   uint8_t allow_multiple_touches_{0x80}; | ||||
|  | ||||
|   GPIOPin *reset_pin_{nullptr}; | ||||
|  | ||||
|   uint8_t cap1188_product_id_{0}; | ||||
|   uint8_t cap1188_manufacture_id_{0}; | ||||
|   uint8_t cap1188_revision_{0}; | ||||
|  | ||||
|   enum ErrorCode { | ||||
|     NONE = 0, | ||||
|     COMMUNICATION_FAILED, | ||||
|   } error_code_{NONE}; | ||||
| }; | ||||
|  | ||||
| }  // namespace cap1188 | ||||
| }  // namespace esphome | ||||
| @@ -36,3 +36,5 @@ async def to_code(config): | ||||
|     if CORE.is_esp32: | ||||
|         cg.add_library("DNSServer", None) | ||||
|         cg.add_library("WiFi", None) | ||||
|     if CORE.is_esp8266: | ||||
|         cg.add_library("DNSServer", None) | ||||
|   | ||||
| @@ -67,6 +67,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { | ||||
|   ESP_LOGI(TAG, "  SSID='%s'", ssid.c_str()); | ||||
|   ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str()); | ||||
|   wifi::global_wifi_component->save_wifi_sta(ssid, psk); | ||||
|   wifi::global_wifi_component->start_scanning(); | ||||
|   request->redirect("/?save=true"); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -52,7 +52,7 @@ void CCS811Component::setup() { | ||||
|  | ||||
|   if (this->baseline_.has_value()) { | ||||
|     // baseline available, write to sensor | ||||
|     this->write_bytes(0x11, decode_uint16(*this->baseline_)); | ||||
|     this->write_bytes(0x11, decode_value(*this->baseline_)); | ||||
|   } | ||||
|  | ||||
|   auto hardware_version_data = this->read_bytes<1>(0x21); | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/cse7761/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/cse7761/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										244
									
								
								esphome/components/cse7761/cse7761.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								esphome/components/cse7761/cse7761.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,244 @@ | ||||
| #include "cse7761.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace cse7761 { | ||||
|  | ||||
| static const char *const TAG = "cse7761"; | ||||
|  | ||||
| /*********************************************************************************************\ | ||||
|  * CSE7761 - Energy  (Sonoff Dual R3 Pow v1.x) | ||||
|  * | ||||
|  * Based on Tasmota source code | ||||
|  * See https://github.com/arendst/Tasmota/discussions/10793 | ||||
|  * https://github.com/arendst/Tasmota/blob/development/tasmota/xnrg_19_cse7761.ino | ||||
| \*********************************************************************************************/ | ||||
|  | ||||
| static const int CSE7761_UREF = 42563;  // RmsUc | ||||
| static const int CSE7761_IREF = 52241;  // RmsIAC | ||||
| static const int CSE7761_PREF = 44513;  // PowerPAC | ||||
|  | ||||
| static const uint8_t CSE7761_REG_SYSCON = 0x00;     // (2) System Control Register (0x0A04) | ||||
| static const uint8_t CSE7761_REG_EMUCON = 0x01;     // (2) Metering control register (0x0000) | ||||
| static const uint8_t CSE7761_REG_EMUCON2 = 0x13;    // (2) Metering control register 2 (0x0001) | ||||
| static const uint8_t CSE7761_REG_PULSE1SEL = 0x1D;  // (2) Pin function output select register (0x3210) | ||||
|  | ||||
| static const uint8_t CSE7761_REG_RMSIA = 0x24;      // (3) The effective value of channel A current (0x000000) | ||||
| static const uint8_t CSE7761_REG_RMSIB = 0x25;      // (3) The effective value of channel B current (0x000000) | ||||
| static const uint8_t CSE7761_REG_RMSU = 0x26;       // (3) Voltage RMS (0x000000) | ||||
| static const uint8_t CSE7761_REG_POWERPA = 0x2C;    // (4) Channel A active power, update rate 27.2Hz (0x00000000) | ||||
| static const uint8_t CSE7761_REG_POWERPB = 0x2D;    // (4) Channel B active power, update rate 27.2Hz (0x00000000) | ||||
| static const uint8_t CSE7761_REG_SYSSTATUS = 0x43;  // (1) System status register | ||||
|  | ||||
| static const uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F;  // (2) Coefficient checksum | ||||
| static const uint8_t CSE7761_REG_RMSIAC = 0x70;       // (2) Channel A effective current conversion coefficient | ||||
|  | ||||
| static const uint8_t CSE7761_SPECIAL_COMMAND = 0xEA;   // Start special command | ||||
| static const uint8_t CSE7761_CMD_RESET = 0x96;         // Reset command, after receiving the command, the chip resets | ||||
| static const uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC;   // Close write operation | ||||
| static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5;  // Enable write operation | ||||
|  | ||||
| enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; | ||||
|  | ||||
| void CSE7761Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up CSE7761..."); | ||||
|   this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_RESET); | ||||
|   uint16_t syscon = this->read_(0x00, 2);  // Default 0x0A04 | ||||
|   if ((0x0A04 == syscon) && this->chip_init_()) { | ||||
|     this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_CLOSE_WRITE); | ||||
|     ESP_LOGD(TAG, "CSE7761 found"); | ||||
|     this->data_.ready = true; | ||||
|   } else { | ||||
|     this->mark_failed(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void CSE7761Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "CSE7761:"); | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "Communication with CSE7761 failed!"); | ||||
|   } | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8); | ||||
| } | ||||
|  | ||||
| float CSE7761Component::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| void CSE7761Component::update() { | ||||
|   if (this->data_.ready) { | ||||
|     this->get_data_(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void CSE7761Component::write_(uint8_t reg, uint16_t data) { | ||||
|   uint8_t buffer[5]; | ||||
|  | ||||
|   buffer[0] = 0xA5; | ||||
|   buffer[1] = reg; | ||||
|   uint32_t len = 2; | ||||
|   if (data) { | ||||
|     if (data < 0xFF) { | ||||
|       buffer[2] = data & 0xFF; | ||||
|       len = 3; | ||||
|     } else { | ||||
|       buffer[2] = (data >> 8) & 0xFF; | ||||
|       buffer[3] = data & 0xFF; | ||||
|       len = 4; | ||||
|     } | ||||
|     uint8_t crc = 0; | ||||
|     for (uint32_t i = 0; i < len; i++) { | ||||
|       crc += buffer[i]; | ||||
|     } | ||||
|     buffer[len] = ~crc; | ||||
|     len++; | ||||
|   } | ||||
|  | ||||
|   this->write_array(buffer, len); | ||||
| } | ||||
|  | ||||
| bool CSE7761Component::read_once_(uint8_t reg, uint8_t size, uint32_t *value) { | ||||
|   while (this->available()) { | ||||
|     this->read(); | ||||
|   } | ||||
|  | ||||
|   this->write_(reg, 0); | ||||
|  | ||||
|   uint8_t buffer[8] = {0}; | ||||
|   uint32_t rcvd = 0; | ||||
|  | ||||
|   for (uint32_t i = 0; i <= size; i++) { | ||||
|     int value = this->read(); | ||||
|     if (value > -1 && rcvd < sizeof(buffer) - 1) { | ||||
|       buffer[rcvd++] = value; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (!rcvd) { | ||||
|     ESP_LOGD(TAG, "Received 0 bytes for register %hhu", reg); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   rcvd--; | ||||
|   uint32_t result = 0; | ||||
|   // CRC check | ||||
|   uint8_t crc = 0xA5 + reg; | ||||
|   for (uint32_t i = 0; i < rcvd; i++) { | ||||
|     result = (result << 8) | buffer[i]; | ||||
|     crc += buffer[i]; | ||||
|   } | ||||
|   crc = ~crc; | ||||
|   if (crc != buffer[rcvd]) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   *value = result; | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) { | ||||
|   bool result = false;  // Start loop | ||||
|   uint8_t retry = 3;    // Retry up to three times | ||||
|   uint32_t value = 0;   // Default no value | ||||
|   while (!result && retry > 0) { | ||||
|     retry--; | ||||
|     if (this->read_once_(reg, size, &value)) | ||||
|       return value; | ||||
|   } | ||||
|   ESP_LOGE(TAG, "Reading register %hhu failed!", reg); | ||||
|   return value; | ||||
| } | ||||
|  | ||||
| uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) { | ||||
|   switch (unit) { | ||||
|     case RMS_UC: | ||||
|       return 0x400000 * 100 / this->data_.coefficient[RMS_UC]; | ||||
|     case RMS_IAC: | ||||
|       return (0x800000 * 100 / this->data_.coefficient[RMS_IAC]) * 10;  // Stay within 32 bits | ||||
|     case POWER_PAC: | ||||
|       return 0x80000000 / this->data_.coefficient[POWER_PAC]; | ||||
|   } | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| bool CSE7761Component::chip_init_() { | ||||
|   uint16_t calc_chksum = 0xFFFF; | ||||
|   for (uint32_t i = 0; i < 8; i++) { | ||||
|     this->data_.coefficient[i] = this->read_(CSE7761_REG_RMSIAC + i, 2); | ||||
|     calc_chksum += this->data_.coefficient[i]; | ||||
|   } | ||||
|   calc_chksum = ~calc_chksum; | ||||
|   uint16_t coeff_chksum = this->read_(CSE7761_REG_COEFFCHKSUM, 2); | ||||
|   if ((calc_chksum != coeff_chksum) || (!calc_chksum)) { | ||||
|     ESP_LOGD(TAG, "Default calibration"); | ||||
|     this->data_.coefficient[RMS_IAC] = CSE7761_IREF; | ||||
|     this->data_.coefficient[RMS_UC] = CSE7761_UREF; | ||||
|     this->data_.coefficient[POWER_PAC] = CSE7761_PREF; | ||||
|   } | ||||
|  | ||||
|   this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_ENABLE_WRITE); | ||||
|  | ||||
|   uint8_t sys_status = this->read_(CSE7761_REG_SYSSTATUS, 1); | ||||
|   if (sys_status & 0x10) {  // Write enable to protected registers (WREN) | ||||
|     this->write_(CSE7761_REG_SYSCON | 0x80, 0xFF04); | ||||
|     this->write_(CSE7761_REG_EMUCON | 0x80, 0x1183); | ||||
|     this->write_(CSE7761_REG_EMUCON2 | 0x80, 0x0FC1); | ||||
|     this->write_(CSE7761_REG_PULSE1SEL | 0x80, 0x3290); | ||||
|   } else { | ||||
|     ESP_LOGD(TAG, "Write failed at chip_init"); | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void CSE7761Component::get_data_() { | ||||
|   // The effective value of current and voltage Rms is a 24-bit signed number, | ||||
|   // the highest bit is 0 for valid data, | ||||
|   //   and when the highest bit is 1, the reading will be processed as zero | ||||
|   // The active power parameter PowerA/B is in two’s complement format, 32-bit | ||||
|   // data, the highest bit is Sign bit. | ||||
|   uint32_t value = this->read_(CSE7761_REG_RMSU, 3); | ||||
|   this->data_.voltage_rms = (value >= 0x800000) ? 0 : value; | ||||
|  | ||||
|   value = this->read_(CSE7761_REG_RMSIA, 3); | ||||
|   this->data_.current_rms[0] = ((value >= 0x800000) || (value < 1600)) ? 0 : value;  // No load threshold of 10mA | ||||
|   value = this->read_(CSE7761_REG_POWERPA, 4); | ||||
|   this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : ((uint32_t) abs((int) value)); | ||||
|  | ||||
|   value = this->read_(CSE7761_REG_RMSIB, 3); | ||||
|   this->data_.current_rms[1] = ((value >= 0x800000) || (value < 1600)) ? 0 : value;  // No load threshold of 10mA | ||||
|   value = this->read_(CSE7761_REG_POWERPB, 4); | ||||
|   this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : ((uint32_t) abs((int) value)); | ||||
|  | ||||
|   // convert values and publish to sensors | ||||
|  | ||||
|   float voltage = (float) this->data_.voltage_rms / this->coefficient_by_unit_(RMS_UC); | ||||
|   if (this->voltage_sensor_ != nullptr) { | ||||
|     this->voltage_sensor_->publish_state(voltage); | ||||
|   } | ||||
|  | ||||
|   for (uint32_t channel = 0; channel < 2; channel++) { | ||||
|     // Active power = PowerPA * PowerPAC * 1000 / 0x80000000 | ||||
|     float active_power = (float) this->data_.active_power[channel] / this->coefficient_by_unit_(POWER_PAC);  // W | ||||
|     float amps = (float) this->data_.current_rms[channel] / this->coefficient_by_unit_(RMS_IAC);             // A | ||||
|     ESP_LOGD(TAG, "Channel %d power %f W, current %f A", channel + 1, active_power, amps); | ||||
|     if (channel == 0) { | ||||
|       if (this->power_sensor_1_ != nullptr) { | ||||
|         this->power_sensor_1_->publish_state(active_power); | ||||
|       } | ||||
|       if (this->current_sensor_1_ != nullptr) { | ||||
|         this->current_sensor_1_->publish_state(amps); | ||||
|       } | ||||
|     } else if (channel == 1) { | ||||
|       if (this->power_sensor_2_ != nullptr) { | ||||
|         this->power_sensor_2_->publish_state(active_power); | ||||
|       } | ||||
|       if (this->current_sensor_2_ != nullptr) { | ||||
|         this->current_sensor_2_->publish_state(amps); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace cse7761 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										52
									
								
								esphome/components/cse7761/cse7761.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/cse7761/cse7761.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/uart/uart.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace cse7761 { | ||||
|  | ||||
| struct CSE7761DataStruct { | ||||
|   uint32_t frequency = 0; | ||||
|   uint32_t voltage_rms = 0; | ||||
|   uint32_t current_rms[2] = {0}; | ||||
|   uint32_t energy[2] = {0}; | ||||
|   uint32_t active_power[2] = {0}; | ||||
|   uint16_t coefficient[8] = {0}; | ||||
|   uint8_t energy_update = 0; | ||||
|   bool ready = false; | ||||
| }; | ||||
|  | ||||
| /// This class implements support for the CSE7761 UART power sensor. | ||||
| class CSE7761Component : public PollingComponent, public uart::UARTDevice { | ||||
|  public: | ||||
|   void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } | ||||
|   void set_active_power_1_sensor(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; } | ||||
|   void set_current_1_sensor(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; } | ||||
|   void set_active_power_2_sensor(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; } | ||||
|   void set_current_2_sensor(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   void update() override; | ||||
|  | ||||
|  protected: | ||||
|   // Sensors | ||||
|   sensor::Sensor *voltage_sensor_{nullptr}; | ||||
|   sensor::Sensor *power_sensor_1_{nullptr}; | ||||
|   sensor::Sensor *current_sensor_1_{nullptr}; | ||||
|   sensor::Sensor *power_sensor_2_{nullptr}; | ||||
|   sensor::Sensor *current_sensor_2_{nullptr}; | ||||
|   CSE7761DataStruct data_; | ||||
|  | ||||
|   void write_(uint8_t reg, uint16_t data); | ||||
|   bool read_once_(uint8_t reg, uint8_t size, uint32_t *value); | ||||
|   uint32_t read_(uint8_t reg, uint8_t size); | ||||
|   uint32_t coefficient_by_unit_(uint32_t unit); | ||||
|   bool chip_init_(); | ||||
|   void get_data_(); | ||||
| }; | ||||
|  | ||||
| }  // namespace cse7761 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										90
									
								
								esphome/components/cse7761/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								esphome/components/cse7761/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, uart | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_VOLTAGE, | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
|     UNIT_AMPERE, | ||||
|     UNIT_WATT, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@berfenger"] | ||||
| DEPENDENCIES = ["uart"] | ||||
|  | ||||
| cse7761_ns = cg.esphome_ns.namespace("cse7761") | ||||
| CSE7761Component = cse7761_ns.class_( | ||||
|     "CSE7761Component", cg.PollingComponent, uart.UARTDevice | ||||
| ) | ||||
|  | ||||
| CONF_CURRENT_1 = "current_1" | ||||
| CONF_CURRENT_2 = "current_2" | ||||
| CONF_ACTIVE_POWER_1 = "active_power_1" | ||||
| CONF_ACTIVE_POWER_2 = "active_power_2" | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(CSE7761Component), | ||||
|             cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_VOLT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_VOLTAGE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_CURRENT_1): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_AMPERE, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_CURRENT, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_CURRENT_2): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_AMPERE, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_CURRENT, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_ACTIVE_POWER_1): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_WATT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_POWER, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_ACTIVE_POWER_2): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_WATT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_POWER, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(uart.UART_DEVICE_SCHEMA) | ||||
| ) | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( | ||||
|     "cse7761", baud_rate=38400, require_rx=True, require_tx=True | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await uart.register_uart_device(var, config) | ||||
|  | ||||
|     for key in [ | ||||
|         CONF_VOLTAGE, | ||||
|         CONF_CURRENT_1, | ||||
|         CONF_CURRENT_2, | ||||
|         CONF_ACTIVE_POWER_1, | ||||
|         CONF_ACTIVE_POWER_2, | ||||
|     ]: | ||||
|         if key not in config: | ||||
|             continue | ||||
|         conf = config[key] | ||||
|         sens = await sensor.new_sensor(conf) | ||||
|         cg.add(getattr(var, f"set_{key}_sensor")(sens)) | ||||
| @@ -6,26 +6,20 @@ from esphome.const import CONF_ID, CONF_PIN | ||||
| MULTI_CONF = True | ||||
| AUTO_LOAD = ["sensor"] | ||||
|  | ||||
| CONF_ONE_WIRE_ID = "one_wire_id" | ||||
| dallas_ns = cg.esphome_ns.namespace("dallas") | ||||
| DallasComponent = dallas_ns.class_("DallasComponent", cg.PollingComponent) | ||||
| ESPOneWire = dallas_ns.class_("ESPOneWire") | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(DallasComponent), | ||||
|             cv.GenerateID(CONF_ONE_WIRE_ID): cv.declare_id(ESPOneWire), | ||||
|             cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, | ||||
|         } | ||||
|     ).extend(cv.polling_component_schema("60s")), | ||||
|     # pin_mode call logs in esp-idf, but InterruptLock is active -> crash | ||||
|     cv.only_with_arduino, | ||||
| ) | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(DallasComponent), | ||||
|         cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, | ||||
|     } | ||||
| ).extend(cv.polling_component_schema("60s")) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||
|     one_wire = cg.new_Pvariable(config[CONF_ONE_WIRE_ID], pin) | ||||
|     var = cg.new_Pvariable(config[CONF_ID], one_wire) | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||
|     cg.add(var.set_pin(pin)) | ||||
|   | ||||
| @@ -31,12 +31,11 @@ uint16_t DallasTemperatureSensor::millis_to_wait_for_conversion() const { | ||||
| void DallasComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up DallasComponent..."); | ||||
|  | ||||
|   yield(); | ||||
|   pin_->setup(); | ||||
|   one_wire_ = new ESPOneWire(pin_);  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|  | ||||
|   std::vector<uint64_t> raw_sensors; | ||||
|   { | ||||
|     InterruptLock lock; | ||||
|     raw_sensors = this->one_wire_->search_vec(); | ||||
|   } | ||||
|   raw_sensors = this->one_wire_->search_vec(); | ||||
|  | ||||
|   for (auto &address : raw_sensors) { | ||||
|     std::string s = uint64_to_string(address); | ||||
| @@ -70,7 +69,7 @@ void DallasComponent::setup() { | ||||
| } | ||||
| void DallasComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "DallasComponent:"); | ||||
|   LOG_PIN("  Pin: ", this->one_wire_->get_pin()); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|  | ||||
|   if (this->found_sensors_.empty()) { | ||||
| @@ -102,15 +101,12 @@ void DallasComponent::update() { | ||||
|   this->status_clear_warning(); | ||||
|  | ||||
|   bool result; | ||||
|   { | ||||
|     InterruptLock lock; | ||||
|     if (!this->one_wire_->reset()) { | ||||
|       result = false; | ||||
|     } else { | ||||
|       result = true; | ||||
|       this->one_wire_->skip(); | ||||
|       this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); | ||||
|     } | ||||
|   if (!this->one_wire_->reset()) { | ||||
|     result = false; | ||||
|   } else { | ||||
|     result = true; | ||||
|     this->one_wire_->skip(); | ||||
|     this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); | ||||
|   } | ||||
|  | ||||
|   if (!result) { | ||||
| @@ -121,11 +117,7 @@ void DallasComponent::update() { | ||||
|  | ||||
|   for (auto *sensor : this->sensors_) { | ||||
|     this->set_timeout(sensor->get_address_name(), sensor->millis_to_wait_for_conversion(), [this, sensor] { | ||||
|       bool res; | ||||
|       { | ||||
|         InterruptLock lock; | ||||
|         res = sensor->read_scratch_pad(); | ||||
|       } | ||||
|       bool res = sensor->read_scratch_pad(); | ||||
|  | ||||
|       if (!res) { | ||||
|         ESP_LOGW(TAG, "'%s' - Resetting bus for read failed!", sensor->get_name().c_str()); | ||||
| @@ -146,7 +138,6 @@ void DallasComponent::update() { | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| DallasComponent::DallasComponent(ESPOneWire *one_wire) : one_wire_(one_wire) {} | ||||
|  | ||||
| void DallasTemperatureSensor::set_address(uint64_t address) { this->address_ = address; } | ||||
| uint8_t DallasTemperatureSensor::get_resolution() const { return this->resolution_; } | ||||
| @@ -162,7 +153,7 @@ const std::string &DallasTemperatureSensor::get_address_name() { | ||||
|   return this->address_name_; | ||||
| } | ||||
| bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { | ||||
|   ESPOneWire *wire = this->parent_->one_wire_; | ||||
|   auto *wire = this->parent_->one_wire_; | ||||
|   if (!wire->reset()) { | ||||
|     return false; | ||||
|   } | ||||
| @@ -176,11 +167,7 @@ bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { | ||||
|   return true; | ||||
| } | ||||
| bool DallasTemperatureSensor::setup_sensor() { | ||||
|   bool r; | ||||
|   { | ||||
|     InterruptLock lock; | ||||
|     r = this->read_scratch_pad(); | ||||
|   } | ||||
|   bool r = this->read_scratch_pad(); | ||||
|  | ||||
|   if (!r) { | ||||
|     ESP_LOGE(TAG, "Reading scratchpad failed: reset"); | ||||
| @@ -214,21 +201,18 @@ bool DallasTemperatureSensor::setup_sensor() { | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   ESPOneWire *wire = this->parent_->one_wire_; | ||||
|   { | ||||
|     InterruptLock lock; | ||||
|     if (wire->reset()) { | ||||
|       wire->select(this->address_); | ||||
|       wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); | ||||
|       wire->write8(this->scratch_pad_[2]);  // high alarm temp | ||||
|       wire->write8(this->scratch_pad_[3]);  // low alarm temp | ||||
|       wire->write8(this->scratch_pad_[4]);  // resolution | ||||
|       wire->reset(); | ||||
|   auto *wire = this->parent_->one_wire_; | ||||
|   if (wire->reset()) { | ||||
|     wire->select(this->address_); | ||||
|     wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); | ||||
|     wire->write8(this->scratch_pad_[2]);  // high alarm temp | ||||
|     wire->write8(this->scratch_pad_[3]);  // low alarm temp | ||||
|     wire->write8(this->scratch_pad_[4]);  // resolution | ||||
|     wire->reset(); | ||||
|  | ||||
|       // write value to EEPROM | ||||
|       wire->select(this->address_); | ||||
|       wire->write8(0x48); | ||||
|     } | ||||
|     // write value to EEPROM | ||||
|     wire->select(this->address_); | ||||
|     wire->write8(0x48); | ||||
|   } | ||||
|  | ||||
|   delay(20);  // allow it to finish operation | ||||
|   | ||||
| @@ -11,8 +11,7 @@ class DallasTemperatureSensor; | ||||
|  | ||||
| class DallasComponent : public PollingComponent { | ||||
|  public: | ||||
|   explicit DallasComponent(ESPOneWire *one_wire); | ||||
|  | ||||
|   void set_pin(InternalGPIOPin *pin) { pin_ = pin; } | ||||
|   void register_sensor(DallasTemperatureSensor *sensor); | ||||
|  | ||||
|   void setup() override; | ||||
| @@ -24,6 +23,7 @@ class DallasComponent : public PollingComponent { | ||||
|  protected: | ||||
|   friend DallasTemperatureSensor; | ||||
|  | ||||
|   InternalGPIOPin *pin_; | ||||
|   ESPOneWire *one_wire_; | ||||
|   std::vector<DallasTemperatureSensor *> sensors_; | ||||
|   std::vector<uint64_t> found_sensors_; | ||||
|   | ||||
| @@ -10,115 +10,123 @@ static const char *const TAG = "dallas.one_wire"; | ||||
| const uint8_t ONE_WIRE_ROM_SELECT = 0x55; | ||||
| const int ONE_WIRE_ROM_SEARCH = 0xF0; | ||||
|  | ||||
| ESPOneWire::ESPOneWire(GPIOPin *pin) : pin_(pin) {} | ||||
| ESPOneWire::ESPOneWire(InternalGPIOPin *pin) { pin_ = pin->to_isr(); } | ||||
|  | ||||
| bool HOT IRAM_ATTR ESPOneWire::reset() { | ||||
|   uint8_t retries = 125; | ||||
|   // See reset here: | ||||
|   // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html | ||||
|   InterruptLock lock; | ||||
|  | ||||
|   // Wait for communication to clear | ||||
|   this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); | ||||
|   // Wait for communication to clear (delay G) | ||||
|   pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); | ||||
|   uint8_t retries = 125; | ||||
|   do { | ||||
|     if (--retries == 0) | ||||
|       return false; | ||||
|     delayMicroseconds(2); | ||||
|   } while (!this->pin_->digital_read()); | ||||
|   } while (!pin_.digital_read()); | ||||
|  | ||||
|   // Send 480µs LOW TX reset pulse | ||||
|   this->pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||
|   this->pin_->digital_write(false); | ||||
|   // Send 480µs LOW TX reset pulse (drive bus low, delay H) | ||||
|   pin_.pin_mode(gpio::FLAG_OUTPUT); | ||||
|   pin_.digital_write(false); | ||||
|   delayMicroseconds(480); | ||||
|  | ||||
|   // Switch into RX mode, letting the pin float | ||||
|   this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); | ||||
|   // after 15µs-60µs wait time, responder pulls low for 60µs-240µs | ||||
|   // let's have 70µs just in case | ||||
|   // Release the bus, delay I | ||||
|   pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); | ||||
|   delayMicroseconds(70); | ||||
|  | ||||
|   bool r = !this->pin_->digital_read(); | ||||
|   // sample bus, 0=device(s) present, 1=no device present | ||||
|   bool r = !pin_.digital_read(); | ||||
|   // delay J | ||||
|   delayMicroseconds(410); | ||||
|   return r; | ||||
| } | ||||
|  | ||||
| void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) { | ||||
|   // Initiate write/read by pulling low. | ||||
|   this->pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||
|   this->pin_->digital_write(false); | ||||
|   // See write 1/0 bit here: | ||||
|   // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html | ||||
|   InterruptLock lock; | ||||
|  | ||||
|   // bus sampled within 15µs and 60µs after pulling LOW. | ||||
|   if (bit) { | ||||
|     // pull high/release within 15µs | ||||
|     delayMicroseconds(10); | ||||
|     this->pin_->digital_write(true); | ||||
|     // in total minimum of 60µs long | ||||
|     delayMicroseconds(55); | ||||
|   } else { | ||||
|     // continue pulling LOW for at least 60µs | ||||
|     delayMicroseconds(65); | ||||
|     this->pin_->digital_write(true); | ||||
|     // grace period, 1µs recovery time | ||||
|     delayMicroseconds(5); | ||||
|   } | ||||
|   // drive bus low | ||||
|   pin_.pin_mode(gpio::FLAG_OUTPUT); | ||||
|   pin_.digital_write(false); | ||||
|  | ||||
|   uint32_t delay0 = bit ? 10 : 65; | ||||
|   uint32_t delay1 = bit ? 55 : 5; | ||||
|  | ||||
|   // delay A/C | ||||
|   delayMicroseconds(delay0); | ||||
|   // release bus | ||||
|   pin_.digital_write(true); | ||||
|   // delay B/D | ||||
|   delayMicroseconds(delay1); | ||||
| } | ||||
|  | ||||
| bool HOT IRAM_ATTR ESPOneWire::read_bit() { | ||||
|   // Initiate read slot by pulling LOW for at least 1µs | ||||
|   this->pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||
|   this->pin_->digital_write(false); | ||||
|   // See read bit here: | ||||
|   // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html | ||||
|   InterruptLock lock; | ||||
|  | ||||
|   // drive bus low, delay A | ||||
|   pin_.pin_mode(gpio::FLAG_OUTPUT); | ||||
|   pin_.digital_write(false); | ||||
|   delayMicroseconds(3); | ||||
|  | ||||
|   // release bus, we have to sample within 15µs of pulling low | ||||
|   this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); | ||||
|   // release bus, delay E | ||||
|   pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); | ||||
|   delayMicroseconds(10); | ||||
|  | ||||
|   bool r = this->pin_->digital_read(); | ||||
|   // read time slot at least 60µs long + 1µs recovery time between slots | ||||
|   // sample bus to read bit from peer | ||||
|   bool r = pin_.digital_read(); | ||||
|  | ||||
|   // delay F | ||||
|   delayMicroseconds(53); | ||||
|   return r; | ||||
| } | ||||
|  | ||||
| void IRAM_ATTR ESPOneWire::write8(uint8_t val) { | ||||
| void ESPOneWire::write8(uint8_t val) { | ||||
|   for (uint8_t i = 0; i < 8; i++) { | ||||
|     this->write_bit(bool((1u << i) & val)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void IRAM_ATTR ESPOneWire::write64(uint64_t val) { | ||||
| void ESPOneWire::write64(uint64_t val) { | ||||
|   for (uint8_t i = 0; i < 64; i++) { | ||||
|     this->write_bit(bool((1ULL << i) & val)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| uint8_t IRAM_ATTR ESPOneWire::read8() { | ||||
| uint8_t ESPOneWire::read8() { | ||||
|   uint8_t ret = 0; | ||||
|   for (uint8_t i = 0; i < 8; i++) { | ||||
|     ret |= (uint8_t(this->read_bit()) << i); | ||||
|   } | ||||
|   return ret; | ||||
| } | ||||
| uint64_t IRAM_ATTR ESPOneWire::read64() { | ||||
| uint64_t ESPOneWire::read64() { | ||||
|   uint64_t ret = 0; | ||||
|   for (uint8_t i = 0; i < 8; i++) { | ||||
|     ret |= (uint64_t(this->read_bit()) << i); | ||||
|   } | ||||
|   return ret; | ||||
| } | ||||
| void IRAM_ATTR ESPOneWire::select(uint64_t address) { | ||||
| void ESPOneWire::select(uint64_t address) { | ||||
|   this->write8(ONE_WIRE_ROM_SELECT); | ||||
|   this->write64(address); | ||||
| } | ||||
| void IRAM_ATTR ESPOneWire::reset_search() { | ||||
| void ESPOneWire::reset_search() { | ||||
|   this->last_discrepancy_ = 0; | ||||
|   this->last_device_flag_ = false; | ||||
|   this->last_family_discrepancy_ = 0; | ||||
|   this->rom_number_ = 0; | ||||
| } | ||||
| uint64_t HOT IRAM_ATTR ESPOneWire::search() { | ||||
| uint64_t ESPOneWire::search() { | ||||
|   if (this->last_device_flag_) { | ||||
|     return 0u; | ||||
|   } | ||||
|  | ||||
|   if (!this->reset()) { | ||||
|     // Reset failed | ||||
|     // Reset failed or no devices present | ||||
|     this->reset_search(); | ||||
|     return 0u; | ||||
|   } | ||||
| @@ -196,7 +204,7 @@ uint64_t HOT IRAM_ATTR ESPOneWire::search() { | ||||
|  | ||||
|   return this->rom_number_; | ||||
| } | ||||
| std::vector<uint64_t> IRAM_ATTR ESPOneWire::search_vec() { | ||||
| std::vector<uint64_t> ESPOneWire::search_vec() { | ||||
|   std::vector<uint64_t> res; | ||||
|  | ||||
|   this->reset_search(); | ||||
| @@ -206,10 +214,9 @@ std::vector<uint64_t> IRAM_ATTR ESPOneWire::search_vec() { | ||||
|  | ||||
|   return res; | ||||
| } | ||||
| void IRAM_ATTR ESPOneWire::skip() { | ||||
| void ESPOneWire::skip() { | ||||
|   this->write8(0xCC);  // skip ROM | ||||
| } | ||||
| GPIOPin *ESPOneWire::get_pin() { return this->pin_; } | ||||
|  | ||||
| uint8_t IRAM_ATTR *ESPOneWire::rom_number8_() { return reinterpret_cast<uint8_t *>(&this->rom_number_); } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include <vector> | ||||
|  | ||||
| @@ -12,7 +11,7 @@ extern const int ONE_WIRE_ROM_SEARCH; | ||||
|  | ||||
| class ESPOneWire { | ||||
|  public: | ||||
|   explicit ESPOneWire(GPIOPin *pin); | ||||
|   explicit ESPOneWire(InternalGPIOPin *pin); | ||||
|  | ||||
|   /** Reset the bus, should be done before all write operations. | ||||
|    * | ||||
| @@ -55,13 +54,11 @@ class ESPOneWire { | ||||
|   /// Helper that wraps search in a std::vector. | ||||
|   std::vector<uint64_t> search_vec(); | ||||
|  | ||||
|   GPIOPin *get_pin(); | ||||
|  | ||||
|  protected: | ||||
|   /// Helper to get the internal 64-bit unsigned rom number as a 8-bit integer pointer. | ||||
|   inline uint8_t *rom_number8_(); | ||||
|  | ||||
|   GPIOPin *pin_; | ||||
|   ISRInternalGPIOPin pin_; | ||||
|   uint8_t last_discrepancy_{0}; | ||||
|   uint8_t last_family_discrepancy_{0}; | ||||
|   bool last_device_flag_{false}; | ||||
|   | ||||
| @@ -29,6 +29,14 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|     } | ||||
| ) | ||||
|  | ||||
| WIFI_MESSAGE = """ | ||||
|  | ||||
| # Do not forget to add your own wifi configuration before installing this configuration | ||||
| # wifi: | ||||
| #   ssid: !secret wifi_ssid | ||||
| #   password: !secret wifi_password | ||||
| """ | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     cg.add_define("USE_DASHBOARD_IMPORT") | ||||
| @@ -41,5 +49,12 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N | ||||
|     if p.exists(): | ||||
|         raise FileExistsError | ||||
|  | ||||
|     config = {"substitutions": {"name": name}, "packages": {project_name: import_url}} | ||||
|     p.write_text(dump(config), encoding="utf8") | ||||
|     config = { | ||||
|         "substitutions": {"name": name}, | ||||
|         "packages": {project_name: import_url}, | ||||
|         "esphome": {"name_add_mac_suffix": False}, | ||||
|     } | ||||
|     p.write_text( | ||||
|         dump(config) + WIFI_MESSAGE, | ||||
|         encoding="utf8", | ||||
|     ) | ||||
|   | ||||
| @@ -78,8 +78,9 @@ void DeepSleepComponent::begin_sleep(bool manual) { | ||||
|     esp_sleep_enable_timer_wakeup(*this->sleep_duration_); | ||||
|   if (this->wakeup_pin_ != nullptr) { | ||||
|     bool level = this->wakeup_pin_->is_inverted(); | ||||
|     if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) | ||||
|     if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && !this->wakeup_pin_->digital_read()) { | ||||
|       level = !level; | ||||
|     } | ||||
|     esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); | ||||
|   } | ||||
|   if (this->ext1_wakeup_.has_value()) { | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import esphome.config_validation as cv | ||||
| from esphome import core, automation | ||||
| from esphome.automation import maybe_simple_id | ||||
| from esphome.const import ( | ||||
|     CONF_AUTO_CLEAR_ENABLED, | ||||
|     CONF_ID, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_PAGES, | ||||
| @@ -79,6 +80,7 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( | ||||
|                 cv.Optional(CONF_TO): cv.use_id(DisplayPage), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| @@ -86,6 +88,10 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( | ||||
| async def setup_display_core_(var, config): | ||||
|     if CONF_ROTATION in config: | ||||
|         cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) | ||||
|  | ||||
|     if CONF_AUTO_CLEAR_ENABLED in config: | ||||
|         cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED])) | ||||
|  | ||||
|     if CONF_PAGES in config: | ||||
|         pages = [] | ||||
|         for conf in config[CONF_PAGES]: | ||||
|   | ||||
| @@ -336,7 +336,9 @@ void DisplayBuffer::show_page(DisplayPage *page) { | ||||
| void DisplayBuffer::show_next_page() { this->page_->show_next(); } | ||||
| void DisplayBuffer::show_prev_page() { this->page_->show_prev(); } | ||||
| void DisplayBuffer::do_update_() { | ||||
|   this->clear(); | ||||
|   if (this->auto_clear_enabled_) { | ||||
|     this->clear(); | ||||
|   } | ||||
|   if (this->page_ != nullptr) { | ||||
|     this->page_->get_writer()(*this); | ||||
|   } else if (this->writer_.has_value()) { | ||||
|   | ||||
| @@ -333,6 +333,9 @@ class DisplayBuffer { | ||||
|   /// Internal method to set the display rotation with. | ||||
|   void set_rotation(DisplayRotation rotation); | ||||
|  | ||||
|   // Internal method to set display auto clearing. | ||||
|   void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; } | ||||
|  | ||||
|  protected: | ||||
|   void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); | ||||
|  | ||||
| @@ -352,6 +355,7 @@ class DisplayBuffer { | ||||
|   DisplayPage *page_{nullptr}; | ||||
|   DisplayPage *previous_page_{nullptr}; | ||||
|   std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_; | ||||
|   bool auto_clear_enabled_{true}; | ||||
| }; | ||||
|  | ||||
| class DisplayPage { | ||||
|   | ||||
| @@ -19,14 +19,30 @@ void Dsmr::loop() { | ||||
|     this->receive_encrypted_(); | ||||
| } | ||||
|  | ||||
| bool Dsmr::available_within_timeout_() { | ||||
|   uint8_t tries = READ_TIMEOUT_MS / 5; | ||||
|   while (tries--) { | ||||
|     delay(5); | ||||
|     if (available()) { | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| void Dsmr::receive_telegram_() { | ||||
|   int count = MAX_BYTES_PER_LOOP; | ||||
|   while (available() && count-- > 0) { | ||||
|   while (true) { | ||||
|     if (!available()) { | ||||
|       if (!header_found_ || !available_within_timeout_()) { | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const char c = read(); | ||||
|  | ||||
|     // Find a new telegram header, i.e. forward slash. | ||||
|     if (c == '/') { | ||||
|       ESP_LOGV(TAG, "Header found"); | ||||
|       ESP_LOGV(TAG, "Header of telegram found"); | ||||
|       header_found_ = true; | ||||
|       footer_found_ = false; | ||||
|       telegram_len_ = 0; | ||||
| @@ -38,7 +54,7 @@ void Dsmr::receive_telegram_() { | ||||
|     if (telegram_len_ >= MAX_TELEGRAM_LENGTH) { | ||||
|       header_found_ = false; | ||||
|       footer_found_ = false; | ||||
|       ESP_LOGE(TAG, "Error: Message larger than buffer"); | ||||
|       ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", MAX_TELEGRAM_LENGTH); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
| @@ -54,16 +70,17 @@ void Dsmr::receive_telegram_() { | ||||
|  | ||||
|     // Check for a footer, i.e. exlamation mark, followed by a hex checksum. | ||||
|     if (c == '!') { | ||||
|       ESP_LOGV(TAG, "Footer found"); | ||||
|       ESP_LOGV(TAG, "Footer of telegram found"); | ||||
|       footer_found_ = true; | ||||
|       continue; | ||||
|     } | ||||
|     // Check for the end of the hex checksum, i.e. a newline. | ||||
|     if (footer_found_ && c == '\n') { | ||||
|       header_found_ = false; | ||||
|       // Parse the telegram and publish sensor values. | ||||
|       if (parse_telegram()) | ||||
|         return; | ||||
|       parse_telegram(); | ||||
|  | ||||
|       header_found_ = false; | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -72,41 +89,46 @@ void Dsmr::receive_encrypted_() { | ||||
|   // Encrypted buffer | ||||
|   uint8_t buffer[MAX_TELEGRAM_LENGTH]; | ||||
|   size_t buffer_length = 0; | ||||
|  | ||||
|   size_t packet_size = 0; | ||||
|   while (available()) { | ||||
|     const char c = read(); | ||||
|  | ||||
|     if (!header_found_) { | ||||
|       if ((uint8_t) c == 0xdb) { | ||||
|         ESP_LOGV(TAG, "Start byte 0xDB found"); | ||||
|         header_found_ = true; | ||||
|   while (true) { | ||||
|     if (!available()) { | ||||
|       if (!header_found_) { | ||||
|         return; | ||||
|       } | ||||
|       if (!available_within_timeout_()) { | ||||
|         ESP_LOGW(TAG, "Timeout while reading data for encrypted telegram"); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Sanity check | ||||
|     if (!header_found_ || buffer_length >= MAX_TELEGRAM_LENGTH) { | ||||
|       if (buffer_length == 0) { | ||||
|         ESP_LOGE(TAG, "First byte of encrypted telegram should be 0xDB, aborting."); | ||||
|       } else { | ||||
|         ESP_LOGW(TAG, "Unexpected data"); | ||||
|     const char c = read(); | ||||
|  | ||||
|     // Find a new telegram start byte. | ||||
|     if (!header_found_) { | ||||
|       if ((uint8_t) c != 0xDB) { | ||||
|         continue; | ||||
|       } | ||||
|       this->status_momentary_warning("unexpected_data"); | ||||
|       this->flush(); | ||||
|       while (available()) | ||||
|         read(); | ||||
|       ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); | ||||
|       header_found_ = true; | ||||
|     } | ||||
|  | ||||
|     // Check for buffer overflow. | ||||
|     if (buffer_length >= MAX_TELEGRAM_LENGTH) { | ||||
|       header_found_ = false; | ||||
|       ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", MAX_TELEGRAM_LENGTH); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     buffer[buffer_length++] = c; | ||||
|  | ||||
|     if (packet_size == 0 && buffer_length > 20) { | ||||
|       // Complete header + a few bytes of data | ||||
|       packet_size = buffer[11] << 8 | buffer[12]; | ||||
|       // Complete header + data bytes | ||||
|       packet_size = 13 + (buffer[11] << 8 | buffer[12]); | ||||
|       ESP_LOGV(TAG, "Encrypted telegram size: %d bytes", packet_size); | ||||
|     } | ||||
|     if (buffer_length == packet_size + 13 && packet_size > 0) { | ||||
|       ESP_LOGV(TAG, "Encrypted data: %d bytes", buffer_length); | ||||
|  | ||||
|     if (buffer_length == packet_size && packet_size > 0) { | ||||
|       ESP_LOGV(TAG, "End of encrypted telegram found"); | ||||
|       GCM<AES128> *gcmaes128{new GCM<AES128>()}; | ||||
|       gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); | ||||
|       // the iv is 8 bytes of the system title + 4 bytes frame counter | ||||
| @@ -123,28 +145,21 @@ void Dsmr::receive_encrypted_() { | ||||
|       delete gcmaes128;  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|  | ||||
|       telegram_len_ = strnlen(this->telegram_, sizeof(this->telegram_)); | ||||
|       ESP_LOGV(TAG, "Decrypted data length: %d", telegram_len_); | ||||
|       ESP_LOGVV(TAG, "Decrypted data %s", this->telegram_); | ||||
|       ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", telegram_len_); | ||||
|       ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); | ||||
|  | ||||
|       parse_telegram(); | ||||
|  | ||||
|       header_found_ = false; | ||||
|       telegram_len_ = 0; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (!available()) { | ||||
|       // baud rate is 115200 for encrypted data, this means a few byte should arrive every time | ||||
|       // program runs faster than buffer loading then available() might return false in the middle | ||||
|       delay(4);  // Wait for data | ||||
|     } | ||||
|   } | ||||
|   if (buffer_length > 0) { | ||||
|     ESP_LOGW(TAG, "Timeout while waiting for encrypted data or invalid data received."); | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool Dsmr::parse_telegram() { | ||||
|   MyData data; | ||||
|   ESP_LOGV(TAG, "Trying to parse"); | ||||
|   ESP_LOGV(TAG, "Trying to parse telegram"); | ||||
|   ::dsmr::ParseResult<void> res = | ||||
|       ::dsmr::P1Parser::parse(&data, telegram_, telegram_len_, false, | ||||
|                               this->crc_check_);  // Parse telegram according to data definition. Ignore unknown values. | ||||
| @@ -161,7 +176,7 @@ bool Dsmr::parse_telegram() { | ||||
| } | ||||
|  | ||||
| void Dsmr::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "dsmr:"); | ||||
|   ESP_LOGCONFIG(TAG, "DSMR:"); | ||||
|  | ||||
| #define DSMR_LOG_SENSOR(s) LOG_SENSOR("  ", #s, this->s_##s##_); | ||||
|   DSMR_SENSOR_LIST(DSMR_LOG_SENSOR, ) | ||||
| @@ -178,12 +193,12 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) { | ||||
|   } | ||||
|  | ||||
|   if (decryption_key.length() != 32) { | ||||
|     ESP_LOGE(TAG, "Error, decryption key must be 32 character long."); | ||||
|     ESP_LOGE(TAG, "Error, decryption key must be 32 character long"); | ||||
|     return; | ||||
|   } | ||||
|   this->decryption_key_.clear(); | ||||
|  | ||||
|   ESP_LOGI(TAG, "Decryption key is set."); | ||||
|   ESP_LOGI(TAG, "Decryption key is set"); | ||||
|   // Verbose level prints decryption key | ||||
|   ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str()); | ||||
|  | ||||
|   | ||||
| @@ -17,8 +17,7 @@ namespace esphome { | ||||
| namespace dsmr { | ||||
|  | ||||
| static constexpr uint32_t MAX_TELEGRAM_LENGTH = 1500; | ||||
| static constexpr uint32_t MAX_BYTES_PER_LOOP = 50; | ||||
| static constexpr uint32_t POLL_TIMEOUT = 1000; | ||||
| static constexpr uint32_t READ_TIMEOUT_MS = 200; | ||||
|  | ||||
| using namespace ::dsmr::fields; | ||||
|  | ||||
| @@ -86,6 +85,17 @@ class Dsmr : public Component, public uart::UARTDevice { | ||||
|   void receive_telegram_(); | ||||
|   void receive_encrypted_(); | ||||
|  | ||||
|   /// Wait for UART data to become available within the read timeout. | ||||
|   /// | ||||
|   /// The smart meter might provide data in chunks, causing available() to | ||||
|   /// return 0. When we're already reading a telegram, then we don't return | ||||
|   /// right away (to handle further data in an upcoming loop) but wait a | ||||
|   /// little while using this method to see if more data are incoming. | ||||
|   /// By not returning, we prevent other components from taking so much | ||||
|   /// time that the UART RX buffer overflows and bytes of the telegram get | ||||
|   /// lost in the process. | ||||
|   bool available_within_timeout_(); | ||||
|  | ||||
|   // Telegram buffer | ||||
|   char telegram_[MAX_TELEGRAM_LENGTH]; | ||||
|   int telegram_len_{0}; | ||||
|   | ||||
| @@ -12,7 +12,6 @@ void DutyCycleSensor::setup() { | ||||
|   this->pin_->setup(); | ||||
|   this->store_.pin = this->pin_->to_isr(); | ||||
|   this->store_.last_level = this->pin_->digital_read(); | ||||
|   this->last_update_ = micros(); | ||||
|   this->store_.last_interrupt = micros(); | ||||
|  | ||||
|   this->pin_->attach_interrupt(DutyCycleSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); | ||||
| @@ -24,19 +23,20 @@ void DutyCycleSensor::dump_config() { | ||||
| } | ||||
| void DutyCycleSensor::update() { | ||||
|   const uint32_t now = micros(); | ||||
|   const bool level = this->store_.last_level; | ||||
|   const uint32_t last_interrupt = this->store_.last_interrupt; | ||||
|   uint32_t on_time = this->store_.on_time; | ||||
|   if (this->last_update_ != 0) { | ||||
|     const bool level = this->store_.last_level; | ||||
|     const uint32_t last_interrupt = this->store_.last_interrupt; | ||||
|     uint32_t on_time = this->store_.on_time; | ||||
|  | ||||
|   if (level) | ||||
|     on_time += now - last_interrupt; | ||||
|     if (level) | ||||
|       on_time += now - last_interrupt; | ||||
|  | ||||
|   const float total_time = float(now - this->last_update_); | ||||
|  | ||||
|   const float value = (on_time / total_time) * 100.0f; | ||||
|   ESP_LOGD(TAG, "'%s' Got duty cycle=%.1f%%", this->get_name().c_str(), value); | ||||
|   this->publish_state(value); | ||||
|     const float total_time = float(now - this->last_update_); | ||||
|  | ||||
|     const float value = (on_time / total_time) * 100.0f; | ||||
|     ESP_LOGD(TAG, "'%s' Got duty cycle=%.1f%%", this->get_name().c_str(), value); | ||||
|     this->publish_state(value); | ||||
|   } | ||||
|   this->store_.on_time = 0; | ||||
|   this->store_.last_interrupt = now; | ||||
|   this->last_update_ = now; | ||||
|   | ||||
| @@ -30,7 +30,7 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { | ||||
|   InternalGPIOPin *pin_; | ||||
|  | ||||
|   DutyCycleSensorStore store_{}; | ||||
|   uint32_t last_update_; | ||||
|   uint32_t last_update_{0}; | ||||
| }; | ||||
|  | ||||
| }  // namespace duty_cycle | ||||
|   | ||||
| @@ -28,6 +28,7 @@ from .const import (  # noqa | ||||
|     KEY_SDKCONFIG_OPTIONS, | ||||
|     KEY_VARIANT, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_FRIENDLY, | ||||
|     VARIANTS, | ||||
| ) | ||||
| from .boards import BOARD_TO_VARIANT | ||||
| @@ -147,8 +148,9 @@ def _arduino_check_versions(value): | ||||
|     value[CONF_VERSION] = str(version) | ||||
|     value[CONF_SOURCE] = source or _format_framework_arduino_version(version) | ||||
|  | ||||
|     platform_version = value.get(CONF_PLATFORM_VERSION, ARDUINO_PLATFORM_VERSION) | ||||
|     value[CONF_PLATFORM_VERSION] = str(platform_version) | ||||
|     value[CONF_PLATFORM_VERSION] = value.get( | ||||
|         CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) | ||||
|     ) | ||||
|  | ||||
|     if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: | ||||
|         _LOGGER.warning( | ||||
| @@ -184,8 +186,9 @@ def _esp_idf_check_versions(value): | ||||
|     value[CONF_VERSION] = str(version) | ||||
|     value[CONF_SOURCE] = source or _format_framework_espidf_version(version) | ||||
|  | ||||
|     platform_version = value.get(CONF_PLATFORM_VERSION, ESP_IDF_PLATFORM_VERSION) | ||||
|     value[CONF_PLATFORM_VERSION] = str(platform_version) | ||||
|     value[CONF_PLATFORM_VERSION] = value.get( | ||||
|         CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION)) | ||||
|     ) | ||||
|  | ||||
|     if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: | ||||
|         _LOGGER.warning( | ||||
| @@ -196,6 +199,15 @@ def _esp_idf_check_versions(value): | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def _parse_platform_version(value): | ||||
|     try: | ||||
|         # if platform version is a valid version constraint, prefix the default package | ||||
|         cv.platformio_version_constraint(value) | ||||
|         return f"platformio/espressif32 @ {value}" | ||||
|     except cv.Invalid: | ||||
|         return value | ||||
|  | ||||
|  | ||||
| def _detect_variant(value): | ||||
|     if CONF_VARIANT not in value: | ||||
|         board = value[CONF_BOARD] | ||||
| @@ -218,7 +230,7 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All( | ||||
|         { | ||||
|             cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, | ||||
|             cv.Optional(CONF_SOURCE): cv.string_strict, | ||||
|             cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, | ||||
|             cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, | ||||
|         } | ||||
|     ), | ||||
|     _arduino_check_versions, | ||||
| @@ -230,7 +242,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||
|         { | ||||
|             cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, | ||||
|             cv.Optional(CONF_SOURCE): cv.string_strict, | ||||
|             cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, | ||||
|             cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, | ||||
|             cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { | ||||
|                 cv.string_strict: cv.string_strict | ||||
|             }, | ||||
| @@ -276,14 +288,14 @@ async def to_code(config): | ||||
|     cg.add_build_flag("-DUSE_ESP32") | ||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||
|     cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") | ||||
|     cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) | ||||
|  | ||||
|     cg.add_platformio_option("lib_ldf_mode", "off") | ||||
|  | ||||
|     conf = config[CONF_FRAMEWORK] | ||||
|     cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) | ||||
|  | ||||
|     if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: | ||||
|         cg.add_platformio_option( | ||||
|             "platform", f"espressif32 @ {conf[CONF_PLATFORM_VERSION]}" | ||||
|         ) | ||||
|         cg.add_platformio_option("framework", "espidf") | ||||
|         cg.add_build_flag("-DUSE_ESP_IDF") | ||||
|         cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") | ||||
| @@ -299,6 +311,8 @@ async def to_code(config): | ||||
|         ) | ||||
|         add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False) | ||||
|         add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_SIZE", True) | ||||
|         # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms | ||||
|         add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000) | ||||
|  | ||||
|         cg.add_platformio_option("board_build.partitions", "partitions.csv") | ||||
|  | ||||
| @@ -312,9 +326,6 @@ async def to_code(config): | ||||
|             ) | ||||
|  | ||||
|     elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: | ||||
|         cg.add_platformio_option( | ||||
|             "platform", f"espressif32 @ {conf[CONF_PLATFORM_VERSION]}" | ||||
|         ) | ||||
|         cg.add_platformio_option("framework", "arduino") | ||||
|         cg.add_build_flag("-DUSE_ARDUINO") | ||||
|         cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") | ||||
|   | ||||
| @@ -18,4 +18,12 @@ VARIANTS = [ | ||||
|     VARIANT_ESP32H2, | ||||
| ] | ||||
|  | ||||
| VARIANT_FRIENDLY = { | ||||
|     VARIANT_ESP32: "ESP32", | ||||
|     VARIANT_ESP32S2: "ESP32-S2", | ||||
|     VARIANT_ESP32S3: "ESP32-S3", | ||||
|     VARIANT_ESP32C3: "ESP32-C3", | ||||
|     VARIANT_ESP32H2: "ESP32-H2", | ||||
| } | ||||
|  | ||||
| esp32_ns = cg.esphome_ns.namespace("esp32") | ||||
|   | ||||
| @@ -21,11 +21,7 @@ void IRAM_ATTR HOT yield() { vPortYield(); } | ||||
| uint32_t IRAM_ATTR HOT millis() { return (uint32_t)(esp_timer_get_time() / 1000ULL); } | ||||
| void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } | ||||
| uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } | ||||
| void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { | ||||
|   auto start = (uint64_t) esp_timer_get_time(); | ||||
|   while (((uint64_t) esp_timer_get_time()) - start < us) | ||||
|     ; | ||||
| } | ||||
| void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } | ||||
| void arch_restart() { | ||||
|   esp_restart(); | ||||
|   // restart() doesn't always end execution | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import logging | ||||
| from dataclasses import dataclass | ||||
| from typing import Any | ||||
|  | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
| @@ -17,10 +18,24 @@ import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| from . import boards | ||||
| from .const import KEY_BOARD, KEY_ESP32, esp32_ns | ||||
| from .const import ( | ||||
|     KEY_BOARD, | ||||
|     KEY_ESP32, | ||||
|     KEY_VARIANT, | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
|     VARIANT_ESP32H2, | ||||
|     esp32_ns, | ||||
| ) | ||||
|  | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports | ||||
| from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports | ||||
| from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports | ||||
| from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports | ||||
| from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports | ||||
|  | ||||
|  | ||||
| IDFInternalGPIOPin = esp32_ns.class_("IDFInternalGPIOPin", cg.InternalGPIOPin) | ||||
| @@ -59,65 +74,61 @@ def _translate_pin(value): | ||||
|     return _lookup_pin(value) | ||||
|  | ||||
|  | ||||
| _ESP_SDIO_PINS = { | ||||
|     6: "Flash Clock", | ||||
|     7: "Flash Data 0", | ||||
|     8: "Flash Data 1", | ||||
|     11: "Flash Command", | ||||
| @dataclass | ||||
| class ESP32ValidationFunctions: | ||||
|     pin_validation: Any | ||||
|     usage_validation: Any | ||||
|  | ||||
|  | ||||
| _esp32_validations = { | ||||
|     VARIANT_ESP32: ESP32ValidationFunctions( | ||||
|         pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports | ||||
|     ), | ||||
|     VARIANT_ESP32S2: ESP32ValidationFunctions( | ||||
|         pin_validation=esp32_s2_validate_gpio_pin, | ||||
|         usage_validation=esp32_s2_validate_supports, | ||||
|     ), | ||||
|     VARIANT_ESP32C3: ESP32ValidationFunctions( | ||||
|         pin_validation=esp32_c3_validate_gpio_pin, | ||||
|         usage_validation=esp32_c3_validate_supports, | ||||
|     ), | ||||
|     VARIANT_ESP32S3: ESP32ValidationFunctions( | ||||
|         pin_validation=esp32_s3_validate_gpio_pin, | ||||
|         usage_validation=esp32_s3_validate_supports, | ||||
|     ), | ||||
|     VARIANT_ESP32H2: ESP32ValidationFunctions( | ||||
|         pin_validation=esp32_h2_validate_gpio_pin, | ||||
|         usage_validation=esp32_h2_validate_supports, | ||||
|     ), | ||||
| } | ||||
|  | ||||
|  | ||||
| def validate_gpio_pin(value): | ||||
|     value = _translate_pin(value) | ||||
|     if value < 0 or value > 39: | ||||
|         raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") | ||||
|     if value in _ESP_SDIO_PINS: | ||||
|         raise cv.Invalid( | ||||
|             f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" | ||||
|         ) | ||||
|     if 9 <= value <= 10: | ||||
|         _LOGGER.warning( | ||||
|             "Pin %s (9-10) might already be used by the " | ||||
|             "flash interface in QUAD IO flash mode.", | ||||
|             value, | ||||
|         ) | ||||
|     if value in (20, 24, 28, 29, 30, 31): | ||||
|         # These pins are not exposed in GPIO mux (reason unknown) | ||||
|         # but they're missing from IO_MUX list in datasheet | ||||
|         raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") | ||||
|     return value | ||||
|     variant = CORE.data[KEY_ESP32][KEY_VARIANT] | ||||
|     if variant not in _esp32_validations: | ||||
|         raise cv.Invalid("Unsupported ESP32 variant {variant}") | ||||
|  | ||||
|     return _esp32_validations[variant].pin_validation(value) | ||||
|  | ||||
|  | ||||
| def validate_supports(value): | ||||
|     num = value[CONF_NUMBER] | ||||
|     mode = value[CONF_MODE] | ||||
|     is_input = mode[CONF_INPUT] | ||||
|     is_output = mode[CONF_OUTPUT] | ||||
|     is_open_drain = mode[CONF_OPEN_DRAIN] | ||||
|     is_pullup = mode[CONF_PULLUP] | ||||
|     is_pulldown = mode[CONF_PULLDOWN] | ||||
|     variant = CORE.data[KEY_ESP32][KEY_VARIANT] | ||||
|     if variant not in _esp32_validations: | ||||
|         raise cv.Invalid("Unsupported ESP32 variant {variant}") | ||||
|  | ||||
|     if is_input: | ||||
|         # All ESP32 pins support input mode | ||||
|         pass | ||||
|     if is_output and 34 <= num <= 39: | ||||
|         raise cv.Invalid( | ||||
|             f"GPIO{num} (34-39) does not support output pin mode.", | ||||
|             [CONF_MODE, CONF_OUTPUT], | ||||
|         ) | ||||
|     if is_open_drain and not is_output: | ||||
|         raise cv.Invalid( | ||||
|             "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] | ||||
|         ) | ||||
|     if is_pullup and 34 <= num <= 39: | ||||
|         raise cv.Invalid( | ||||
|             f"GPIO{num} (34-39) does not support pullups.", [CONF_MODE, CONF_PULLUP] | ||||
|         ) | ||||
|     if is_pulldown and 34 <= num <= 39: | ||||
|         raise cv.Invalid( | ||||
|             f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] | ||||
|         ) | ||||
|  | ||||
|     value = _esp32_validations[variant].usage_validation(value) | ||||
|     if CORE.using_arduino: | ||||
|         # (input, output, open_drain, pullup, pulldown) | ||||
|         supported_modes = { | ||||
| @@ -138,7 +149,6 @@ def validate_supports(value): | ||||
|                 "This pin mode is not supported on ESP32 for arduino frameworks", | ||||
|                 [CONF_MODE], | ||||
|             ) | ||||
|  | ||||
|     return value | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,22 @@ namespace esp32 { | ||||
|  | ||||
| static const char *const TAG = "esp32"; | ||||
|  | ||||
| static int IRAM_ATTR flags_to_mode(gpio::Flags flags) { | ||||
|   if (flags == gpio::FLAG_INPUT) { | ||||
|     return INPUT; | ||||
|   } else if (flags == gpio::FLAG_OUTPUT) { | ||||
|     return OUTPUT; | ||||
|   } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { | ||||
|     return INPUT_PULLUP; | ||||
|   } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { | ||||
|     return INPUT_PULLDOWN; | ||||
|   } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { | ||||
|     return OUTPUT_OPEN_DRAIN; | ||||
|   } else { | ||||
|     return 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| struct ISRPinArg { | ||||
|   uint8_t pin; | ||||
|   bool inverted; | ||||
| @@ -43,22 +59,9 @@ void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, g | ||||
|  | ||||
|   attachInterruptArg(pin_, func, arg, arduino_mode); | ||||
| } | ||||
|  | ||||
| void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) { | ||||
|   uint8_t mode; | ||||
|   if (flags == gpio::FLAG_INPUT) { | ||||
|     mode = INPUT; | ||||
|   } else if (flags == gpio::FLAG_OUTPUT) { | ||||
|     mode = OUTPUT; | ||||
|   } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { | ||||
|     mode = INPUT_PULLUP; | ||||
|   } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { | ||||
|     mode = INPUT_PULLDOWN; | ||||
|   } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { | ||||
|     mode = OUTPUT_OPEN_DRAIN; | ||||
|   } else { | ||||
|     return; | ||||
|   } | ||||
|   pinMode(pin_, mode);  // NOLINT | ||||
|   pinMode(pin_, flags_to_mode(flags));  // NOLINT | ||||
| } | ||||
|  | ||||
| std::string ArduinoInternalGPIOPin::dump_summary() const { | ||||
| @@ -101,6 +104,10 @@ void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { | ||||
|   auto *arg = reinterpret_cast<ISRPinArg *>(arg_); | ||||
|   pinMode(arg->pin, flags_to_mode(flags));  // NOLINT | ||||
| } | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
							
								
								
									
										77
									
								
								esphome/components/esp32/gpio_esp32.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								esphome/components/esp32/gpio_esp32.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome.const import ( | ||||
|     CONF_INPUT, | ||||
|     CONF_MODE, | ||||
|     CONF_NUMBER, | ||||
|     CONF_OUTPUT, | ||||
|     CONF_PULLDOWN, | ||||
|     CONF_PULLUP, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
|  | ||||
| _ESP_SDIO_PINS = { | ||||
|     6: "Flash Clock", | ||||
|     7: "Flash Data 0", | ||||
|     8: "Flash Data 1", | ||||
|     11: "Flash Command", | ||||
| } | ||||
|  | ||||
| _ESP32_STRAPPING_PINS = {0, 2, 4, 15} | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| def esp32_validate_gpio_pin(value): | ||||
|     if value < 0 or value > 39: | ||||
|         raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") | ||||
|     if value in _ESP_SDIO_PINS: | ||||
|         raise cv.Invalid( | ||||
|             f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" | ||||
|         ) | ||||
|     if 9 <= value <= 10: | ||||
|         _LOGGER.warning( | ||||
|             "Pin %s (9-10) might already be used by the " | ||||
|             "flash interface in QUAD IO flash mode.", | ||||
|             value, | ||||
|         ) | ||||
|     if value in _ESP32_STRAPPING_PINS: | ||||
|         _LOGGER.warning( | ||||
|             "GPIO%d is a Strapping PIN and should be avoided.\n" | ||||
|             "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" | ||||
|             "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", | ||||
|             value, | ||||
|         ) | ||||
|     if value in (20, 24, 28, 29, 30, 31): | ||||
|         # These pins are not exposed in GPIO mux (reason unknown) | ||||
|         # but they're missing from IO_MUX list in datasheet | ||||
|         raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def esp32_validate_supports(value): | ||||
|     num = value[CONF_NUMBER] | ||||
|     mode = value[CONF_MODE] | ||||
|     is_input = mode[CONF_INPUT] | ||||
|     is_output = mode[CONF_OUTPUT] | ||||
|     is_pullup = mode[CONF_PULLUP] | ||||
|     is_pulldown = mode[CONF_PULLDOWN] | ||||
|  | ||||
|     if is_input: | ||||
|         # All ESP32 pins support input mode | ||||
|         pass | ||||
|     if is_output and 34 <= num <= 39: | ||||
|         raise cv.Invalid( | ||||
|             f"GPIO{num} (34-39) does not support output pin mode.", | ||||
|             [CONF_MODE, CONF_OUTPUT], | ||||
|         ) | ||||
|     if is_pullup and 34 <= num <= 39: | ||||
|         raise cv.Invalid( | ||||
|             f"GPIO{num} (34-39) does not support pullups.", [CONF_MODE, CONF_PULLUP] | ||||
|         ) | ||||
|     if is_pulldown and 34 <= num <= 39: | ||||
|         raise cv.Invalid( | ||||
|             f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] | ||||
|         ) | ||||
|  | ||||
|     return value | ||||
							
								
								
									
										53
									
								
								esphome/components/esp32/gpio_esp32_c3.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								esphome/components/esp32/gpio_esp32_c3.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome.const import ( | ||||
|     CONF_INPUT, | ||||
|     CONF_MODE, | ||||
|     CONF_NUMBER, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| _ESP32C3_SPI_PSRAM_PINS = { | ||||
|     12: "SPIHD", | ||||
|     13: "SPIWP", | ||||
|     14: "SPICS0", | ||||
|     15: "SPICLK", | ||||
|     16: "SPID", | ||||
|     17: "SPIQ", | ||||
| } | ||||
|  | ||||
| _ESP32C3_STRAPPING_PINS = {2, 8, 9} | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| def esp32_c3_validate_gpio_pin(value): | ||||
|     if value < 0 or value > 21: | ||||
|         raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") | ||||
|     if value in _ESP32C3_SPI_PSRAM_PINS: | ||||
|         raise cv.Invalid( | ||||
|             f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" | ||||
|         ) | ||||
|     if value in _ESP32C3_STRAPPING_PINS: | ||||
|         _LOGGER.warning( | ||||
|             "GPIO%d is a Strapping PIN and should be avoided.\n" | ||||
|             "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" | ||||
|             "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", | ||||
|             value, | ||||
|         ) | ||||
|  | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def esp32_c3_validate_supports(value): | ||||
|     num = value[CONF_NUMBER] | ||||
|     mode = value[CONF_MODE] | ||||
|     is_input = mode[CONF_INPUT] | ||||
|  | ||||
|     if num < 0 or num > 21: | ||||
|         raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") | ||||
|  | ||||
|     if is_input: | ||||
|         # All ESP32 pins support input mode | ||||
|         pass | ||||
|     return value | ||||
							
								
								
									
										11
									
								
								esphome/components/esp32/gpio_esp32_h2.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								esphome/components/esp32/gpio_esp32_h2.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
|  | ||||
| def esp32_h2_validate_gpio_pin(value): | ||||
|     # ESP32-H2 not yet supported | ||||
|     raise cv.Invalid("ESP32-H2 isn't supported yet") | ||||
|  | ||||
|  | ||||
| def esp32_h2_validate_supports(value): | ||||
|     # ESP32-H2 not yet supported | ||||
|     raise cv.Invalid("ESP32-H2 isn't supported yet") | ||||
							
								
								
									
										80
									
								
								esphome/components/esp32/gpio_esp32_s2.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								esphome/components/esp32/gpio_esp32_s2.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome.const import ( | ||||
|     CONF_INPUT, | ||||
|     CONF_MODE, | ||||
|     CONF_NUMBER, | ||||
|     CONF_OUTPUT, | ||||
|     CONF_PULLDOWN, | ||||
|     CONF_PULLUP, | ||||
| ) | ||||
|  | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| _ESP32S2_SPI_PSRAM_PINS = { | ||||
|     26: "SPICS1", | ||||
|     27: "SPIHD", | ||||
|     28: "SPIWP", | ||||
|     29: "SPICS0", | ||||
|     30: "SPICLK", | ||||
|     31: "SPIQ", | ||||
|     32: "SPID", | ||||
| } | ||||
|  | ||||
| _ESP32S2_STRAPPING_PINS = {0, 45, 46} | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| def esp32_s2_validate_gpio_pin(value): | ||||
|     if value < 0 or value > 46: | ||||
|         raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") | ||||
|  | ||||
|     if value in _ESP32S2_SPI_PSRAM_PINS: | ||||
|         raise cv.Invalid( | ||||
|             f"This pin cannot be used on ESP32-S2s and is already used by the SPI/PSRAM interface (function: {_ESP32S2_SPI_PSRAM_PINS[value]})" | ||||
|         ) | ||||
|     if value in _ESP32S2_STRAPPING_PINS: | ||||
|         _LOGGER.warning( | ||||
|             "GPIO%d is a Strapping PIN and should be avoided.\n" | ||||
|             "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" | ||||
|             "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", | ||||
|             value, | ||||
|         ) | ||||
|  | ||||
|     if value in (22, 23, 24, 25): | ||||
|         # These pins are not exposed in GPIO mux (reason unknown) | ||||
|         # but they're missing from IO_MUX list in datasheet | ||||
|         raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S2s.") | ||||
|  | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def esp32_s2_validate_supports(value): | ||||
|     num = value[CONF_NUMBER] | ||||
|     mode = value[CONF_MODE] | ||||
|     is_input = mode[CONF_INPUT] | ||||
|     is_output = mode[CONF_OUTPUT] | ||||
|     is_pullup = mode[CONF_PULLUP] | ||||
|     is_pulldown = mode[CONF_PULLDOWN] | ||||
|  | ||||
|     if num < 0 or num > 46: | ||||
|         raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") | ||||
|     if is_input: | ||||
|         # All ESP32 pins support input mode | ||||
|         pass | ||||
|     if is_output and num == 46: | ||||
|         raise cv.Invalid( | ||||
|             f"GPIO{num} does not support output pin mode.", | ||||
|             [CONF_MODE, CONF_OUTPUT], | ||||
|         ) | ||||
|     if is_pullup and num == 46: | ||||
|         raise cv.Invalid( | ||||
|             f"GPIO{num} does not support pullups.", [CONF_MODE, CONF_PULLUP] | ||||
|         ) | ||||
|     if is_pulldown and num == 46: | ||||
|         raise cv.Invalid( | ||||
|             f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN] | ||||
|         ) | ||||
|  | ||||
|     return value | ||||
							
								
								
									
										74
									
								
								esphome/components/esp32/gpio_esp32_s3.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								esphome/components/esp32/gpio_esp32_s3.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome.const import ( | ||||
|     CONF_INPUT, | ||||
|     CONF_MODE, | ||||
|     CONF_NUMBER, | ||||
| ) | ||||
|  | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| _ESP_32S3_SPI_PSRAM_PINS = { | ||||
|     26: "SPICS1", | ||||
|     27: "SPIHD", | ||||
|     28: "SPIWP", | ||||
|     29: "SPICS0", | ||||
|     30: "SPICLK", | ||||
|     31: "SPIQ", | ||||
|     32: "SPID", | ||||
| } | ||||
|  | ||||
| _ESP_32_ESP32_S3R8_PSRAM_PINS = { | ||||
|     33: "SPIIO4", | ||||
|     34: "SPIIO5", | ||||
|     35: "SPIIO6", | ||||
|     36: "SPIIO7", | ||||
|     37: "SPIDQS", | ||||
| } | ||||
|  | ||||
| _ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46} | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| def esp32_s3_validate_gpio_pin(value): | ||||
|     if value < 0 or value > 48: | ||||
|         raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") | ||||
|  | ||||
|     if value in _ESP_32S3_SPI_PSRAM_PINS: | ||||
|         raise cv.Invalid( | ||||
|             f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP_32S3_SPI_PSRAM_PINS[value]})" | ||||
|         ) | ||||
|     if value in _ESP_32_ESP32_S3R8_PSRAM_PINS: | ||||
|         _LOGGER.warning( | ||||
|             "GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models", | ||||
|             value, | ||||
|         ) | ||||
|  | ||||
|     if value in _ESP_32S3_STRAPPING_PINS: | ||||
|         _LOGGER.warning( | ||||
|             "GPIO%d is a Strapping PIN and should be avoided.\n" | ||||
|             "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" | ||||
|             "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", | ||||
|             value, | ||||
|         ) | ||||
|  | ||||
|     if value in (22, 23, 24, 25): | ||||
|         # These pins are not exposed in GPIO mux (reason unknown) | ||||
|         # but they're missing from IO_MUX list in datasheet | ||||
|         raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.") | ||||
|  | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def esp32_s3_validate_supports(value): | ||||
|     num = value[CONF_NUMBER] | ||||
|     mode = value[CONF_MODE] | ||||
|     is_input = mode[CONF_INPUT] | ||||
|  | ||||
|     if num < 0 or num > 48: | ||||
|         raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") | ||||
|     if is_input: | ||||
|         # All ESP32 pins support input mode | ||||
|         pass | ||||
|     return value | ||||
| @@ -10,38 +10,7 @@ static const char *const TAG = "esp32"; | ||||
|  | ||||
| bool IDFInternalGPIOPin::isr_service_installed = false;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| struct ISRPinArg { | ||||
|   gpio_num_t pin; | ||||
|   bool inverted; | ||||
| }; | ||||
|  | ||||
| ISRInternalGPIOPin IDFInternalGPIOPin::to_isr() const { | ||||
|   auto *arg = new ISRPinArg{};  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|   arg->pin = pin_; | ||||
|   arg->inverted = inverted_; | ||||
|   return ISRInternalGPIOPin((void *) arg); | ||||
| } | ||||
|  | ||||
| void IDFInternalGPIOPin::setup() { | ||||
|   pin_mode(flags_); | ||||
|   gpio_set_drive_capability(pin_, drive_strength_); | ||||
| } | ||||
|  | ||||
| void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) { | ||||
|   gpio_config_t conf{}; | ||||
|   conf.pin_bit_mask = 1ULL << static_cast<uint32_t>(pin_); | ||||
|   conf.mode = flags_to_mode(flags); | ||||
|   conf.pull_up_en = flags & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; | ||||
|   conf.pull_down_en = flags & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; | ||||
|   conf.intr_type = GPIO_INTR_DISABLE; | ||||
|   gpio_config(&conf); | ||||
| } | ||||
|  | ||||
| bool IDFInternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } | ||||
|  | ||||
| void IDFInternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } | ||||
|  | ||||
| gpio_mode_t IDFInternalGPIOPin::flags_to_mode(gpio::Flags flags) { | ||||
| static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) { | ||||
|   flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)); | ||||
|   if (flags == gpio::FLAG_NONE) { | ||||
|     return GPIO_MODE_DISABLE; | ||||
| @@ -61,6 +30,18 @@ gpio_mode_t IDFInternalGPIOPin::flags_to_mode(gpio::Flags flags) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| struct ISRPinArg { | ||||
|   gpio_num_t pin; | ||||
|   bool inverted; | ||||
| }; | ||||
|  | ||||
| ISRInternalGPIOPin IDFInternalGPIOPin::to_isr() const { | ||||
|   auto *arg = new ISRPinArg{};  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|   arg->pin = pin_; | ||||
|   arg->inverted = inverted_; | ||||
|   return ISRInternalGPIOPin((void *) arg); | ||||
| } | ||||
|  | ||||
| void IDFInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { | ||||
|   gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE; | ||||
|   switch (type) { | ||||
| @@ -99,6 +80,35 @@ std::string IDFInternalGPIOPin::dump_summary() const { | ||||
|   return buffer; | ||||
| } | ||||
|  | ||||
| void IDFInternalGPIOPin::setup() { | ||||
|   gpio_config_t conf{}; | ||||
|   conf.pin_bit_mask = 1ULL << static_cast<uint32_t>(pin_); | ||||
|   conf.mode = flags_to_mode(flags_); | ||||
|   conf.pull_up_en = flags_ & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; | ||||
|   conf.pull_down_en = flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; | ||||
|   conf.intr_type = GPIO_INTR_DISABLE; | ||||
|   gpio_config(&conf); | ||||
|   gpio_set_drive_capability(pin_, drive_strength_); | ||||
| } | ||||
|  | ||||
| void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) { | ||||
|   // can't call gpio_config here because that logs in esp-idf which may cause issues | ||||
|   gpio_set_direction(pin_, flags_to_mode(flags)); | ||||
|   gpio_pull_mode_t pull_mode = GPIO_FLOATING; | ||||
|   if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) { | ||||
|     pull_mode = GPIO_PULLUP_PULLDOWN; | ||||
|   } else if (flags & gpio::FLAG_PULLUP) { | ||||
|     pull_mode = GPIO_PULLUP_ONLY; | ||||
|   } else if (flags & gpio::FLAG_PULLDOWN) { | ||||
|     pull_mode = GPIO_PULLDOWN_ONLY; | ||||
|   } | ||||
|   gpio_set_pull_mode(pin_, pull_mode); | ||||
| } | ||||
|  | ||||
| bool IDFInternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } | ||||
| void IDFInternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } | ||||
| void IDFInternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } | ||||
|  | ||||
| }  // namespace esp32 | ||||
|  | ||||
| using namespace esp32; | ||||
| @@ -114,6 +124,19 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { | ||||
| void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { | ||||
|   // not supported | ||||
| } | ||||
| void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { | ||||
|   auto *arg = reinterpret_cast<ISRPinArg *>(arg_); | ||||
|   gpio_set_direction(arg->pin, flags_to_mode(flags)); | ||||
|   gpio_pull_mode_t pull_mode = GPIO_FLOATING; | ||||
|   if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) { | ||||
|     pull_mode = GPIO_PULLUP_PULLDOWN; | ||||
|   } else if (flags & gpio::FLAG_PULLUP) { | ||||
|     pull_mode = GPIO_PULLUP_ONLY; | ||||
|   } else if (flags & gpio::FLAG_PULLDOWN) { | ||||
|     pull_mode = GPIO_PULLDOWN_ONLY; | ||||
|   } | ||||
|   gpio_set_pull_mode(arg->pin, pull_mode); | ||||
| } | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -18,13 +18,12 @@ class IDFInternalGPIOPin : public InternalGPIOPin { | ||||
|   bool digital_read() override; | ||||
|   void digital_write(bool value) override; | ||||
|   std::string dump_summary() const override; | ||||
|   void detach_interrupt() const override { gpio_intr_disable(pin_); } | ||||
|   void detach_interrupt() const override; | ||||
|   ISRInternalGPIOPin to_isr() const override; | ||||
|   uint8_t get_pin() const override { return (uint8_t) pin_; } | ||||
|   bool is_inverted() const override { return inverted_; } | ||||
|  | ||||
|  protected: | ||||
|   static gpio_mode_t flags_to_mode(gpio::Flags flags); | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   gpio_num_t pin_; | ||||
|   | ||||
| @@ -18,7 +18,6 @@ from esphome.core import CORE | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option | ||||
|  | ||||
| DEPENDENCIES = ["esp32"] | ||||
| AUTO_LOAD = ["xiaomi_ble", "ruuvi_ble"] | ||||
|  | ||||
| CONF_ESP32_BLE_ID = "esp32_ble_id" | ||||
| CONF_SCAN_PARAMETERS = "scan_parameters" | ||||
|   | ||||
| @@ -2,10 +2,8 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.const import ( | ||||
|     CONF_DISABLED_BY_DEFAULT, | ||||
|     CONF_FREQUENCY, | ||||
|     CONF_ID, | ||||
|     CONF_NAME, | ||||
|     CONF_PIN, | ||||
|     CONF_SCL, | ||||
|     CONF_SDA, | ||||
| @@ -17,8 +15,9 @@ from esphome.const import ( | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| DEPENDENCIES = ["esp32", "api"] | ||||
| DEPENDENCIES = ["esp32"] | ||||
|  | ||||
| esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") | ||||
| ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) | ||||
| @@ -63,11 +62,9 @@ CONF_TEST_PATTERN = "test_pattern" | ||||
|  | ||||
| camera_range_param = cv.int_range(min=-2, max=2) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
| CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(ESP32Camera), | ||||
|         cv.Required(CONF_NAME): cv.string, | ||||
|         cv.Optional(CONF_DISABLED_BY_DEFAULT, default=False): cv.boolean, | ||||
|         cv.Required(CONF_DATA_PINS): cv.All( | ||||
|             [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) | ||||
|         ), | ||||
| @@ -127,8 +124,8 @@ SETTERS = { | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME]) | ||||
|     cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await setup_entity(var, config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     for key, setter in SETTERS.items(): | ||||
|   | ||||
| @@ -45,6 +45,7 @@ void ESP32Camera::dump_config() { | ||||
|   auto conf = this->config_; | ||||
|   ESP_LOGCONFIG(TAG, "ESP32 Camera:"); | ||||
|   ESP_LOGCONFIG(TAG, "  Name: %s", this->name_.c_str()); | ||||
|   ESP_LOGCONFIG(TAG, "  Internal: %s", YESNO(this->internal_)); | ||||
| #ifdef USE_ARDUINO | ||||
|   ESP_LOGCONFIG(TAG, "  Board Has PSRAM: %s", YESNO(psramFound())); | ||||
| #endif  // USE_ARDUINO | ||||
| @@ -185,6 +186,7 @@ ESP32Camera::ESP32Camera(const std::string &name) : EntityBase(name) { | ||||
|  | ||||
|   global_esp32_camera = this; | ||||
| } | ||||
| ESP32Camera::ESP32Camera() : ESP32Camera("") {} | ||||
| void ESP32Camera::set_data_pins(std::array<uint8_t, 8> pins) { | ||||
|   this->config_.pin_d0 = pins[0]; | ||||
|   this->config_.pin_d1 = pins[1]; | ||||
|   | ||||
| @@ -54,6 +54,7 @@ enum ESP32CameraFrameSize { | ||||
| class ESP32Camera : public Component, public EntityBase { | ||||
|  public: | ||||
|   ESP32Camera(const std::string &name); | ||||
|   ESP32Camera(); | ||||
|   void set_data_pins(std::array<uint8_t, 8> pins); | ||||
|   void set_vsync_pin(uint8_t pin); | ||||
|   void set_href_pin(uint8_t pin); | ||||
|   | ||||
							
								
								
									
										28
									
								
								esphome/components/esp32_camera_web_server/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								esphome/components/esp32_camera_web_server/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
| from esphome.const import CONF_ID, CONF_PORT, CONF_MODE | ||||
|  | ||||
| CODEOWNERS = ["@ayufan"] | ||||
| DEPENDENCIES = ["esp32_camera"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server") | ||||
| CameraWebServer = esp32_camera_web_server_ns.class_("CameraWebServer", cg.Component) | ||||
| Mode = esp32_camera_web_server_ns.enum("Mode") | ||||
|  | ||||
| MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT} | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(CameraWebServer), | ||||
|         cv.Required(CONF_PORT): cv.port, | ||||
|         cv.Required(CONF_MODE): cv.enum(MODES, upper=True), | ||||
|     }, | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     server = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(server.set_port(config[CONF_PORT])) | ||||
|     cg.add(server.set_mode(config[CONF_MODE])) | ||||
|     await cg.register_component(server, config) | ||||
							
								
								
									
										240
									
								
								esphome/components/esp32_camera_web_server/camera_web_server.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								esphome/components/esp32_camera_web_server/camera_web_server.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,240 @@ | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "camera_web_server.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/util.h" | ||||
|  | ||||
| #include <cstdlib> | ||||
| #include <esp_http_server.h> | ||||
| #include <utility> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_camera_web_server { | ||||
|  | ||||
| static const int IMAGE_REQUEST_TIMEOUT = 2000; | ||||
| static const char *const TAG = "esp32_camera_web_server"; | ||||
|  | ||||
| #define PART_BOUNDARY "123456789000000000000987654321" | ||||
| #define CONTENT_TYPE "image/jpeg" | ||||
| #define CONTENT_LENGTH "Content-Length" | ||||
|  | ||||
| static const char *const STREAM_HEADER = | ||||
|     "HTTP/1.1 200\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: multipart/x-mixed-replace;boundary=" PART_BOUNDARY | ||||
|     "\r\n"; | ||||
| static const char *const STREAM_500 = "HTTP/1.1 500\r\nContent-Type: text/plain\r\n\r\nNo frames send.\r\n"; | ||||
| static const char *const STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; | ||||
| static const char *const STREAM_PART = "Content-Type: " CONTENT_TYPE "\r\n" CONTENT_LENGTH ": %u\r\n\r\n"; | ||||
|  | ||||
| CameraWebServer::CameraWebServer() {} | ||||
|  | ||||
| CameraWebServer::~CameraWebServer() {} | ||||
|  | ||||
| void CameraWebServer::setup() { | ||||
|   if (!esp32_camera::global_esp32_camera || esp32_camera::global_esp32_camera->is_failed()) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->semaphore_ = xSemaphoreCreateBinary(); | ||||
|  | ||||
|   httpd_config_t config = HTTPD_DEFAULT_CONFIG(); | ||||
|   config.server_port = this->port_; | ||||
|   config.ctrl_port = this->port_; | ||||
|   config.max_open_sockets = 1; | ||||
|   config.backlog_conn = 2; | ||||
|  | ||||
|   if (httpd_start(&this->httpd_, &config) != ESP_OK) { | ||||
|     mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   httpd_uri_t uri = { | ||||
|       .uri = "/", | ||||
|       .method = HTTP_GET, | ||||
|       .handler = [](struct httpd_req *req) { return ((CameraWebServer *) req->user_ctx)->handler_(req); }, | ||||
|       .user_ctx = this}; | ||||
|  | ||||
|   httpd_register_uri_handler(this->httpd_, &uri); | ||||
|  | ||||
|   esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr<esp32_camera::CameraImage> image) { | ||||
|     if (this->running_) { | ||||
|       this->image_ = std::move(image); | ||||
|       xSemaphoreGive(this->semaphore_); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void CameraWebServer::on_shutdown() { | ||||
|   this->running_ = false; | ||||
|   this->image_ = nullptr; | ||||
|   httpd_stop(this->httpd_); | ||||
|   this->httpd_ = nullptr; | ||||
|   vSemaphoreDelete(this->semaphore_); | ||||
|   this->semaphore_ = nullptr; | ||||
| } | ||||
|  | ||||
| void CameraWebServer::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "ESP32 Camera Web Server:"); | ||||
|   ESP_LOGCONFIG(TAG, "  Port: %d", this->port_); | ||||
|   if (this->mode_ == STREAM) | ||||
|     ESP_LOGCONFIG(TAG, "  Mode: stream"); | ||||
|   else | ||||
|     ESP_LOGCONFIG(TAG, "  Mode: snapshot"); | ||||
|  | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "  Setup Failed"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| float CameraWebServer::get_setup_priority() const { return setup_priority::LATE; } | ||||
|  | ||||
| void CameraWebServer::loop() { | ||||
|   if (!this->running_) { | ||||
|     this->image_ = nullptr; | ||||
|   } | ||||
| } | ||||
|  | ||||
| std::shared_ptr<esphome::esp32_camera::CameraImage> CameraWebServer::wait_for_image_() { | ||||
|   std::shared_ptr<esphome::esp32_camera::CameraImage> image; | ||||
|   image.swap(this->image_); | ||||
|  | ||||
|   if (!image) { | ||||
|     // retry as we might still be fetching image | ||||
|     xSemaphoreTake(this->semaphore_, IMAGE_REQUEST_TIMEOUT / portTICK_PERIOD_MS); | ||||
|     image.swap(this->image_); | ||||
|   } | ||||
|  | ||||
|   return image; | ||||
| } | ||||
|  | ||||
| esp_err_t CameraWebServer::handler_(struct httpd_req *req) { | ||||
|   esp_err_t res = ESP_FAIL; | ||||
|  | ||||
|   this->image_ = nullptr; | ||||
|   this->running_ = true; | ||||
|  | ||||
|   switch (this->mode_) { | ||||
|     case STREAM: | ||||
|       res = this->streaming_handler_(req); | ||||
|       break; | ||||
|  | ||||
|     case SNAPSHOT: | ||||
|       res = this->snapshot_handler_(req); | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   this->running_ = false; | ||||
|   this->image_ = nullptr; | ||||
|   return res; | ||||
| } | ||||
|  | ||||
| static esp_err_t httpd_send_all(httpd_req_t *r, const char *buf, size_t buf_len) { | ||||
|   int ret; | ||||
|  | ||||
|   while (buf_len > 0) { | ||||
|     ret = httpd_send(r, buf, buf_len); | ||||
|     if (ret < 0) { | ||||
|       return ESP_FAIL; | ||||
|     } | ||||
|     buf += ret; | ||||
|     buf_len -= ret; | ||||
|   } | ||||
|   return ESP_OK; | ||||
| } | ||||
|  | ||||
| esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { | ||||
|   esp_err_t res = ESP_OK; | ||||
|   char part_buf[64]; | ||||
|  | ||||
|   // This manually constructs HTTP response to avoid chunked encoding | ||||
|   // which is not supported by some clients | ||||
|  | ||||
|   res = httpd_send_all(req, STREAM_HEADER, strlen(STREAM_HEADER)); | ||||
|   if (res != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "STREAM: failed to set HTTP header"); | ||||
|     return res; | ||||
|   } | ||||
|  | ||||
|   uint32_t last_frame = millis(); | ||||
|   uint32_t frames = 0; | ||||
|  | ||||
|   while (res == ESP_OK && this->running_) { | ||||
|     if (esp32_camera::global_esp32_camera != nullptr) { | ||||
|       esp32_camera::global_esp32_camera->request_stream(); | ||||
|     } | ||||
|  | ||||
|     auto image = this->wait_for_image_(); | ||||
|  | ||||
|     if (!image) { | ||||
|       ESP_LOGW(TAG, "STREAM: failed to acquire frame"); | ||||
|       res = ESP_FAIL; | ||||
|     } | ||||
|     if (res == ESP_OK) { | ||||
|       res = httpd_send_all(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY)); | ||||
|     } | ||||
|     if (res == ESP_OK) { | ||||
|       size_t hlen = snprintf(part_buf, 64, STREAM_PART, image->get_data_length()); | ||||
|       res = httpd_send_all(req, part_buf, hlen); | ||||
|     } | ||||
|     if (res == ESP_OK) { | ||||
|       res = httpd_send_all(req, (const char *) image->get_data_buffer(), image->get_data_length()); | ||||
|     } | ||||
|     if (res == ESP_OK) { | ||||
|       frames++; | ||||
|       int64_t frame_time = millis() - last_frame; | ||||
|       last_frame = millis(); | ||||
|  | ||||
|       ESP_LOGD(TAG, "MJPG: %uB %ums (%.1ffps)", (uint32_t) image->get_data_length(), (uint32_t) frame_time, | ||||
|                1000.0 / (uint32_t) frame_time); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (!frames) { | ||||
|     res = httpd_send_all(req, STREAM_500, strlen(STREAM_500)); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGI(TAG, "STREAM: closed. Frames: %u", frames); | ||||
|  | ||||
|   return res; | ||||
| } | ||||
|  | ||||
| esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) { | ||||
|   esp_err_t res = ESP_OK; | ||||
|  | ||||
|   if (esp32_camera::global_esp32_camera != nullptr) { | ||||
|     esp32_camera::global_esp32_camera->request_image(); | ||||
|   } | ||||
|  | ||||
|   auto image = this->wait_for_image_(); | ||||
|  | ||||
|   if (!image) { | ||||
|     ESP_LOGW(TAG, "SNAPSHOT: failed to acquire frame"); | ||||
|     httpd_resp_send_500(req); | ||||
|     res = ESP_FAIL; | ||||
|     return res; | ||||
|   } | ||||
|  | ||||
|   res = httpd_resp_set_type(req, CONTENT_TYPE); | ||||
|   if (res != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "SNAPSHOT: failed to set HTTP response type"); | ||||
|     return res; | ||||
|   } | ||||
|  | ||||
|   httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg"); | ||||
|  | ||||
|   if (res == ESP_OK) { | ||||
|     res = httpd_resp_set_hdr(req, CONTENT_LENGTH, esphome::to_string(image->get_data_length()).c_str()); | ||||
|   } | ||||
|   if (res == ESP_OK) { | ||||
|     res = httpd_resp_send(req, (const char *) image->get_data_buffer(), image->get_data_length()); | ||||
|   } | ||||
|   return res; | ||||
| } | ||||
|  | ||||
| }  // namespace esp32_camera_web_server | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
| @@ -0,0 +1,51 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <freertos/FreeRTOS.h> | ||||
| #include <freertos/semphr.h> | ||||
|  | ||||
| #include "esphome/components/esp32_camera/esp32_camera.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/preferences.h" | ||||
|  | ||||
| struct httpd_req; | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_camera_web_server { | ||||
|  | ||||
| enum Mode { STREAM, SNAPSHOT }; | ||||
|  | ||||
| class CameraWebServer : public Component { | ||||
|  public: | ||||
|   CameraWebServer(); | ||||
|   ~CameraWebServer(); | ||||
|  | ||||
|   void setup() override; | ||||
|   void on_shutdown() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   void set_port(uint16_t port) { this->port_ = port; } | ||||
|   void set_mode(Mode mode) { this->mode_ = mode; } | ||||
|   void loop() override; | ||||
|  | ||||
|  protected: | ||||
|   std::shared_ptr<esphome::esp32_camera::CameraImage> wait_for_image_(); | ||||
|   esp_err_t handler_(struct httpd_req *req); | ||||
|   esp_err_t streaming_handler_(struct httpd_req *req); | ||||
|   esp_err_t snapshot_handler_(struct httpd_req *req); | ||||
|  | ||||
|  protected: | ||||
|   uint16_t port_{0}; | ||||
|   void *httpd_{nullptr}; | ||||
|   SemaphoreHandle_t semaphore_; | ||||
|   std::shared_ptr<esphome::esp32_camera::CameraImage> image_; | ||||
|   bool running_{false}; | ||||
|   Mode mode_{STREAM}; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_camera_web_server | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
| @@ -11,6 +11,7 @@ namespace esphome { | ||||
| namespace esp32_improv { | ||||
|  | ||||
| static const char *const TAG = "esp32_improv.component"; | ||||
| static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; | ||||
|  | ||||
| ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } | ||||
|  | ||||
| @@ -124,8 +125,13 @@ void ESP32ImprovComponent::loop() { | ||||
|         this->cancel_timeout("wifi-connect-timeout"); | ||||
|         this->set_state_(improv::STATE_PROVISIONED); | ||||
|  | ||||
|         std::string url = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; | ||||
|         std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, {url}); | ||||
|         std::vector<std::string> urls = {ESPHOME_MY_LINK}; | ||||
| #ifdef USE_WEBSERVER | ||||
|         auto ip = wifi::global_wifi_component->wifi_sta_ip(); | ||||
|         std::string webserver_url = "http://" + ip.str() + ":" + to_string(WEBSERVER_PORT); | ||||
|         urls.push_back(webserver_url); | ||||
| #endif | ||||
|         std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls); | ||||
|         this->send_response_(data); | ||||
|         this->set_timeout("end-service", 1000, [this] { | ||||
|           this->service_->stop(); | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "esp32_touch.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| @@ -93,7 +94,6 @@ void ESP32TouchComponent::dump_config() { | ||||
|  | ||||
|   if (this->iir_filter_enabled_()) { | ||||
|     ESP_LOGCONFIG(TAG, "    IIR Filter: %ums", this->iir_filter_); | ||||
|     touch_pad_filter_start(this->iir_filter_); | ||||
|   } else { | ||||
|     ESP_LOGCONFIG(TAG, "  IIR Filter DISABLED"); | ||||
|   } | ||||
| @@ -125,6 +125,8 @@ void ESP32TouchComponent::loop() { | ||||
|     if (should_print) { | ||||
|       ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value); | ||||
|     } | ||||
|  | ||||
|     App.feed_wdt(); | ||||
|   } | ||||
|  | ||||
|   if (should_print) { | ||||
|   | ||||
| @@ -93,12 +93,12 @@ def _arduino_check_versions(value): | ||||
|     platform_version = value.get(CONF_PLATFORM_VERSION) | ||||
|     if platform_version is None: | ||||
|         if version >= cv.Version(3, 0, 0): | ||||
|             platform_version = ARDUINO_3_PLATFORM_VERSION | ||||
|             platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION)) | ||||
|         elif version >= cv.Version(2, 5, 0): | ||||
|             platform_version = ARDUINO_2_PLATFORM_VERSION | ||||
|             platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION)) | ||||
|         else: | ||||
|             platform_version = cv.Version(1, 8, 0) | ||||
|     value[CONF_PLATFORM_VERSION] = str(platform_version) | ||||
|             platform_version = _parse_platform_version(str(cv.Version(1, 8, 0))) | ||||
|     value[CONF_PLATFORM_VERSION] = platform_version | ||||
|  | ||||
|     if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: | ||||
|         _LOGGER.warning( | ||||
| @@ -109,13 +109,22 @@ def _arduino_check_versions(value): | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def _parse_platform_version(value): | ||||
|     try: | ||||
|         # if platform version is a valid version constraint, prefix the default package | ||||
|         cv.platformio_version_constraint(value) | ||||
|         return f"platformio/espressif8266 @ {value}" | ||||
|     except cv.Invalid: | ||||
|         return value | ||||
|  | ||||
|  | ||||
| CONF_PLATFORM_VERSION = "platform_version" | ||||
| ARDUINO_FRAMEWORK_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, | ||||
|             cv.Optional(CONF_SOURCE): cv.string_strict, | ||||
|             cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, | ||||
|             cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, | ||||
|         } | ||||
|     ), | ||||
|     _arduino_check_versions, | ||||
| @@ -142,21 +151,22 @@ CONFIG_SCHEMA = cv.All( | ||||
| async def to_code(config): | ||||
|     cg.add(esp8266_ns.setup_preferences()) | ||||
|  | ||||
|     cg.add_platformio_option("lib_ldf_mode", "off") | ||||
|  | ||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||
|     cg.add_build_flag("-DUSE_ESP8266") | ||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||
|     cg.add_define("ESPHOME_VARIANT", "ESP8266") | ||||
|  | ||||
|     conf = config[CONF_FRAMEWORK] | ||||
|     cg.add_platformio_option("framework", "arduino") | ||||
|     cg.add_build_flag("-DUSE_ARDUINO") | ||||
|     cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO") | ||||
|     cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) | ||||
|     cg.add_platformio_option( | ||||
|         "platform_packages", | ||||
|         [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_SOURCE]}"], | ||||
|     ) | ||||
|     cg.add_platformio_option( | ||||
|         "platform", f"platformio/espressif8266 @ {conf[CONF_PLATFORM_VERSION]}" | ||||
|     ) | ||||
|  | ||||
|     # Default for platformio is LWIP2_LOW_MEMORY with: | ||||
|     #  - MSS=536 | ||||
|   | ||||
| @@ -12,7 +12,7 @@ void IRAM_ATTR HOT yield() { ::yield(); } | ||||
| uint32_t IRAM_ATTR HOT millis() { return ::millis(); } | ||||
| void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } | ||||
| uint32_t IRAM_ATTR HOT micros() { return ::micros(); } | ||||
| void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } | ||||
| void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } | ||||
| void arch_restart() { | ||||
|   ESP.restart();  // NOLINT(readability-static-accessed-through-instance) | ||||
|   // restart() doesn't always end execution | ||||
|   | ||||
| @@ -8,6 +8,29 @@ namespace esp8266 { | ||||
|  | ||||
| static const char *const TAG = "esp8266"; | ||||
|  | ||||
| static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { | ||||
|   if (flags == gpio::FLAG_INPUT) { | ||||
|     return INPUT; | ||||
|   } else if (flags == gpio::FLAG_OUTPUT) { | ||||
|     return OUTPUT; | ||||
|   } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { | ||||
|     if (pin == 16) { | ||||
|       // GPIO16 doesn't have a pullup, so pinMode would fail. | ||||
|       // However, sometimes this method is called with pullup mode anyway | ||||
|       // for example from dallas one_wire. For those cases convert this | ||||
|       // to a INPUT mode. | ||||
|       return INPUT; | ||||
|     } | ||||
|     return INPUT_PULLUP; | ||||
|   } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { | ||||
|     return INPUT_PULLDOWN_16; | ||||
|   } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { | ||||
|     return OUTPUT_OPEN_DRAIN; | ||||
|   } else { | ||||
|     return 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| struct ISRPinArg { | ||||
|   uint8_t pin; | ||||
|   bool inverted; | ||||
| @@ -43,28 +66,7 @@ void ESP8266GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::Int | ||||
|   attachInterruptArg(pin_, func, arg, arduino_mode); | ||||
| } | ||||
| void ESP8266GPIOPin::pin_mode(gpio::Flags flags) { | ||||
|   uint8_t mode; | ||||
|   if (flags == gpio::FLAG_INPUT) { | ||||
|     mode = INPUT; | ||||
|   } else if (flags == gpio::FLAG_OUTPUT) { | ||||
|     mode = OUTPUT; | ||||
|   } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { | ||||
|     mode = INPUT_PULLUP; | ||||
|     if (pin_ == 16) { | ||||
|       // GPIO16 doesn't have a pullup, so pinMode would fail. | ||||
|       // However, sometimes this method is called with pullup mode anyway | ||||
|       // for example from dallas one_wire. For those cases convert this | ||||
|       // to a INPUT mode. | ||||
|       mode = INPUT; | ||||
|     } | ||||
|   } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { | ||||
|     mode = INPUT_PULLDOWN_16; | ||||
|   } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { | ||||
|     mode = OUTPUT_OPEN_DRAIN; | ||||
|   } else { | ||||
|     return; | ||||
|   } | ||||
|   pinMode(pin_, mode);  // NOLINT | ||||
|   pinMode(pin_, flags_to_mode(flags, pin_));  // NOLINT | ||||
| } | ||||
|  | ||||
| std::string ESP8266GPIOPin::dump_summary() const { | ||||
| @@ -97,6 +99,10 @@ void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { | ||||
|   auto *arg = reinterpret_cast<ISRPinArg *>(arg_); | ||||
|   GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin); | ||||
| } | ||||
| void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { | ||||
|   auto *arg = reinterpret_cast<ISRPinArg *>(arg_); | ||||
|   pinMode(arg->pin, flags_to_mode(flags, arg->pin));  // NOLINT | ||||
| } | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -168,7 +168,9 @@ void EthernetComponent::start_connect_() { | ||||
|  | ||||
|   esp_err_t err; | ||||
|   err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_ETH, App.get_name().c_str()); | ||||
|   ESPHL_ERROR_CHECK(err, "ETH set hostname error"); | ||||
|   if (err != ERR_OK) { | ||||
|     ESP_LOGW(TAG, "tcpip_adapter_set_hostname failed: %s", esp_err_to_name(err)); | ||||
|   } | ||||
|  | ||||
|   tcpip_adapter_ip_info_t info; | ||||
|   if (this->manual_ip_.has_value()) { | ||||
|   | ||||
| @@ -43,19 +43,27 @@ def validate_source_shorthand(value): | ||||
|     # Regex for GitHub repo name with optional branch/tag | ||||
|     # Note: git allows other branch/tag names as well, but never seen them used before | ||||
|     m = re.match( | ||||
|         r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?", | ||||
|         r"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?|pr#([0-9]+))", | ||||
|         value, | ||||
|     ) | ||||
|     if m is None: | ||||
|         raise cv.Invalid( | ||||
|             "Source is not a file system path or in expected github://username/name[@branch-or-tag] format!" | ||||
|             "Source is not a file system path, in expected github://username/name[@branch-or-tag] or github://pr#1234 format!" | ||||
|         ) | ||||
|     conf = { | ||||
|         CONF_TYPE: TYPE_GIT, | ||||
|         CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", | ||||
|     } | ||||
|     if m.group(3): | ||||
|         conf[CONF_REF] = m.group(3) | ||||
|     if m.group(4): | ||||
|         conf = { | ||||
|             CONF_TYPE: TYPE_GIT, | ||||
|             CONF_URL: "https://github.com/esphome/esphome.git", | ||||
|             CONF_REF: f"pull/{m.group(4)}/head", | ||||
|         } | ||||
|     else: | ||||
|         conf = { | ||||
|             CONF_TYPE: TYPE_GIT, | ||||
|             CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", | ||||
|         } | ||||
|         if m.group(3): | ||||
|             conf[CONF_REF] = m.group(3) | ||||
|  | ||||
|     return SOURCE_SCHEMA(conf) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -74,7 +74,7 @@ void EZOSensor::loop() { | ||||
|   if (buf[0] != 1) | ||||
|     return; | ||||
|  | ||||
|   float val = strtof((char *) &buf[1], nullptr); | ||||
|   float val = parse_number<float>((char *) &buf[1], sizeof(buf) - 1).value_or(0); | ||||
|   this->publish_state(val); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,7 @@ from esphome.const import ( | ||||
|     CONF_LAST_FINGER_ID, | ||||
|     CONF_SECURITY_LEVEL, | ||||
|     CONF_STATUS, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     ICON_ACCOUNT, | ||||
|     ICON_ACCOUNT_CHECK, | ||||
|     ICON_DATABASE, | ||||
| @@ -26,30 +27,36 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|             icon=ICON_FINGERPRINT, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_NONE, | ||||
|             entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|         ), | ||||
|         cv.Optional(CONF_STATUS): sensor.sensor_schema( | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_NONE, | ||||
|             entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|         ), | ||||
|         cv.Optional(CONF_CAPACITY): sensor.sensor_schema( | ||||
|             icon=ICON_DATABASE, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_NONE, | ||||
|             entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|         ), | ||||
|         cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( | ||||
|             icon=ICON_SECURITY, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_NONE, | ||||
|             entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|         ), | ||||
|         cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( | ||||
|             icon=ICON_ACCOUNT, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_NONE, | ||||
|             entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|         ), | ||||
|         cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( | ||||
|             icon=ICON_ACCOUNT_CHECK, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_NONE, | ||||
|             entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ static const char *const TAG = "homeassistant.sensor"; | ||||
| void HomeassistantSensor::setup() { | ||||
|   api::global_api_server->subscribe_home_assistant_state( | ||||
|       this->entity_id_, this->attribute_, [this](const std::string &state) { | ||||
|         auto val = parse_float(state); | ||||
|         auto val = parse_number<float>(state); | ||||
|         if (!val.has_value()) { | ||||
|           ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); | ||||
|           this->publish_state(NAN); | ||||
|   | ||||
| @@ -44,7 +44,7 @@ void HrxlMaxsonarWrComponent::check_buffer_() { | ||||
|  | ||||
|     if (this->buffer_.length() == MAX_DATA_LENGTH_BYTES && this->buffer_[0] == 'R' && | ||||
|         this->buffer_.back() == static_cast<char>(ASCII_CR)) { | ||||
|       int millimeters = strtol(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2).c_str(), nullptr, 10); | ||||
|       int millimeters = parse_number<int>(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2)).value_or(0); | ||||
|       float meters = float(millimeters) / 1000.0; | ||||
|       ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters); | ||||
|       this->publish_state(meters); | ||||
|   | ||||
| @@ -96,6 +96,8 @@ async def to_code(config): | ||||
|     if CORE.is_esp32: | ||||
|         cg.add_library("WiFiClientSecure", None) | ||||
|         cg.add_library("HTTPClient", None) | ||||
|     if CORE.is_esp8266: | ||||
|         cg.add_library("ESP8266HTTPClient", None) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|   | ||||
| @@ -9,8 +9,8 @@ static const char *const TAG = "htu21d"; | ||||
|  | ||||
| static const uint8_t HTU21D_ADDRESS = 0x40; | ||||
| static const uint8_t HTU21D_REGISTER_RESET = 0xFE; | ||||
| static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xE3; | ||||
| static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xE5; | ||||
| static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xF3; | ||||
| static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xF5; | ||||
| static const uint8_t HTU21D_REGISTER_STATUS = 0xE7; | ||||
|  | ||||
| void HTU21DComponent::setup() { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "i2c_bus.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/optional.h" | ||||
| #include <array> | ||||
| #include <vector> | ||||
| @@ -32,16 +33,8 @@ class I2CRegister { | ||||
|  | ||||
| // like ntohs/htons but without including networking headers. | ||||
| // ("i2c" byte order is big-endian) | ||||
| inline uint16_t i2ctohs(uint16_t i2cshort) { | ||||
|   union { | ||||
|     uint16_t x; | ||||
|     uint8_t y[2]; | ||||
|   } conv; | ||||
|   conv.x = i2cshort; | ||||
|   return ((uint16_t) conv.y[0] << 8) | ((uint16_t) conv.y[1] << 0); | ||||
| } | ||||
|  | ||||
| inline uint16_t htoi2cs(uint16_t hostshort) { return i2ctohs(hostshort); } | ||||
| inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } | ||||
| inline uint16_t htoi2cs(uint16_t hostshort) { return convert_big_endian(hostshort); } | ||||
|  | ||||
| class I2CDevice { | ||||
|  public: | ||||
|   | ||||
| @@ -2,27 +2,32 @@ | ||||
|  | ||||
| namespace improv { | ||||
|  | ||||
| ImprovCommand parse_improv_data(const std::vector<uint8_t> &data) { | ||||
|   return parse_improv_data(data.data(), data.size()); | ||||
| ImprovCommand parse_improv_data(const std::vector<uint8_t> &data, bool check_checksum) { | ||||
|   return parse_improv_data(data.data(), data.size(), check_checksum); | ||||
| } | ||||
|  | ||||
| ImprovCommand parse_improv_data(const uint8_t *data, size_t length) { | ||||
| ImprovCommand parse_improv_data(const uint8_t *data, size_t length, bool check_checksum) { | ||||
|   ImprovCommand improv_command; | ||||
|   Command command = (Command) data[0]; | ||||
|   uint8_t data_length = data[1]; | ||||
|  | ||||
|   if (data_length != length - 3) { | ||||
|     return {.command = UNKNOWN}; | ||||
|   if (data_length != length - 2 - check_checksum) { | ||||
|     improv_command.command = UNKNOWN; | ||||
|     return improv_command; | ||||
|   } | ||||
|  | ||||
|   uint8_t checksum = data[length - 1]; | ||||
|   if (check_checksum) { | ||||
|     uint8_t checksum = data[length - 1]; | ||||
|  | ||||
|   uint32_t calculated_checksum = 0; | ||||
|   for (uint8_t i = 0; i < length - 1; i++) { | ||||
|     calculated_checksum += data[i]; | ||||
|   } | ||||
|     uint32_t calculated_checksum = 0; | ||||
|     for (uint8_t i = 0; i < length - 1; i++) { | ||||
|       calculated_checksum += data[i]; | ||||
|     } | ||||
|  | ||||
|   if ((uint8_t) calculated_checksum != checksum) { | ||||
|     return {.command = BAD_CHECKSUM}; | ||||
|     if ((uint8_t) calculated_checksum != checksum) { | ||||
|       improv_command.command = BAD_CHECKSUM; | ||||
|       return improv_command; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (command == WIFI_SETTINGS) { | ||||
| @@ -39,12 +44,11 @@ ImprovCommand parse_improv_data(const uint8_t *data, size_t length) { | ||||
|     return {.command = command, .ssid = ssid, .password = password}; | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|       .command = command, | ||||
|   }; | ||||
|   improv_command.command = command; | ||||
|   return improv_command; | ||||
| } | ||||
|  | ||||
| std::vector<uint8_t> build_rpc_response(Command command, const std::vector<std::string> &datum) { | ||||
| std::vector<uint8_t> build_rpc_response(Command command, const std::vector<std::string> &datum, bool add_checksum) { | ||||
|   std::vector<uint8_t> out; | ||||
|   uint32_t length = 0; | ||||
|   out.push_back(command); | ||||
| @@ -56,17 +60,19 @@ std::vector<uint8_t> build_rpc_response(Command command, const std::vector<std:: | ||||
|   } | ||||
|   out.insert(out.begin() + 1, length); | ||||
|  | ||||
|   uint32_t calculated_checksum = 0; | ||||
|   if (add_checksum) { | ||||
|     uint32_t calculated_checksum = 0; | ||||
|  | ||||
|   for (uint8_t byte : out) { | ||||
|     calculated_checksum += byte; | ||||
|     for (uint8_t byte : out) { | ||||
|       calculated_checksum += byte; | ||||
|     } | ||||
|     out.push_back(calculated_checksum); | ||||
|   } | ||||
|   out.push_back(calculated_checksum); | ||||
|   return out; | ||||
| } | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
| std::vector<uint8_t> build_rpc_response(Command command, const std::vector<String> &datum) { | ||||
| #ifdef ARDUINO | ||||
| std::vector<uint8_t> build_rpc_response(Command command, const std::vector<String> &datum, bool add_checksum) { | ||||
|   std::vector<uint8_t> out; | ||||
|   uint32_t length = 0; | ||||
|   out.push_back(command); | ||||
| @@ -78,14 +84,16 @@ std::vector<uint8_t> build_rpc_response(Command command, const std::vector<Strin | ||||
|   } | ||||
|   out.insert(out.begin() + 1, length); | ||||
|  | ||||
|   uint32_t calculated_checksum = 0; | ||||
|   if (add_checksum) { | ||||
|     uint32_t calculated_checksum = 0; | ||||
|  | ||||
|   for (uint8_t byte : out) { | ||||
|     calculated_checksum += byte; | ||||
|     for (uint8_t byte : out) { | ||||
|       calculated_checksum += byte; | ||||
|     } | ||||
|     out.push_back(calculated_checksum); | ||||
|   } | ||||
|   out.push_back(calculated_checksum); | ||||
|   return out; | ||||
| } | ||||
| #endif  // USE_ARDUINO | ||||
| #endif  // ARDUINO | ||||
|  | ||||
| }  // namespace improv | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
| #ifdef ARDUINO | ||||
| #include "WString.h" | ||||
| #endif  // USE_ARDUINO | ||||
| #endif  // ARDUINO | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <string> | ||||
| @@ -38,6 +38,8 @@ enum Command : uint8_t { | ||||
|   UNKNOWN = 0x00, | ||||
|   WIFI_SETTINGS = 0x01, | ||||
|   IDENTIFY = 0x02, | ||||
|   GET_CURRENT_STATE = 0x02, | ||||
|   GET_DEVICE_INFO = 0x03, | ||||
|   BAD_CHECKSUM = 0xFF, | ||||
| }; | ||||
|  | ||||
| @@ -49,12 +51,13 @@ struct ImprovCommand { | ||||
|   std::string password; | ||||
| }; | ||||
|  | ||||
| ImprovCommand parse_improv_data(const std::vector<uint8_t> &data); | ||||
| ImprovCommand parse_improv_data(const uint8_t *data, size_t length); | ||||
| ImprovCommand parse_improv_data(const std::vector<uint8_t> &data, bool check_checksum = true); | ||||
| ImprovCommand parse_improv_data(const uint8_t *data, size_t length, bool check_checksum = true); | ||||
|  | ||||
| std::vector<uint8_t> build_rpc_response(Command command, const std::vector<std::string> &datum); | ||||
| #ifdef USE_ARDUINO | ||||
| std::vector<uint8_t> build_rpc_response(Command command, const std::vector<String> &datum); | ||||
| #endif  // USE_ARDUINO | ||||
| std::vector<uint8_t> build_rpc_response(Command command, const std::vector<std::string> &datum, | ||||
|                                         bool add_checksum = true); | ||||
| #ifdef ARDUINO | ||||
| std::vector<uint8_t> build_rpc_response(Command command, const std::vector<String> &datum, bool add_checksum = true); | ||||
| #endif  // ARDUINO | ||||
|  | ||||
| }  // namespace improv | ||||
|   | ||||
							
								
								
									
										33
									
								
								esphome/components/improv_serial/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/improv_serial/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| DEPENDENCIES = ["logger", "wifi"] | ||||
| AUTO_LOAD = ["improv"] | ||||
|  | ||||
| improv_serial_ns = cg.esphome_ns.namespace("improv_serial") | ||||
|  | ||||
| ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(ImprovSerialComponent), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| def validate_logger_baud_rate(config): | ||||
|     logger_conf = fv.full_config.get()[CONF_LOGGER] | ||||
|     if logger_conf[CONF_BAUD_RATE] == 0: | ||||
|         raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
							
								
								
									
										266
									
								
								esphome/components/improv_serial/improv_serial_component.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								esphome/components/improv_serial/improv_serial_component.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,266 @@ | ||||
| #include "improv_serial_component.h" | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/version.h" | ||||
|  | ||||
| #include "esphome/components/logger/logger.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace improv_serial { | ||||
|  | ||||
| static const char *const TAG = "improv_serial"; | ||||
|  | ||||
| void ImprovSerialComponent::setup() { | ||||
|   global_improv_serial_component = this; | ||||
| #ifdef USE_ARDUINO | ||||
|   this->hw_serial_ = logger::global_logger->get_hw_serial(); | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
|   this->uart_num_ = logger::global_logger->get_uart_num(); | ||||
| #endif | ||||
|  | ||||
|   if (wifi::global_wifi_component->has_sta()) { | ||||
|     this->state_ = improv::STATE_PROVISIONED; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } | ||||
|  | ||||
| int ImprovSerialComponent::available_() { | ||||
| #ifdef USE_ARDUINO | ||||
|   return this->hw_serial_->available(); | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
|   size_t available; | ||||
|   uart_get_buffered_data_len(this->uart_num_, &available); | ||||
|   return available; | ||||
| #endif | ||||
| } | ||||
|  | ||||
| uint8_t ImprovSerialComponent::read_byte_() { | ||||
|   uint8_t data; | ||||
| #ifdef USE_ARDUINO | ||||
|   this->hw_serial_->readBytes(&data, 1); | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
|   uart_read_bytes(this->uart_num_, &data, 1, 20 / portTICK_RATE_MS); | ||||
| #endif | ||||
|   return data; | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) { | ||||
|   data.push_back('\n'); | ||||
| #ifdef USE_ARDUINO | ||||
|   this->hw_serial_->write(data.data(), data.size()); | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
|   uart_write_bytes(this->uart_num_, data.data(), data.size()); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::loop() { | ||||
|   const uint32_t now = millis(); | ||||
|   if (now - this->last_read_byte_ > 50) { | ||||
|     this->rx_buffer_.clear(); | ||||
|     this->last_read_byte_ = now; | ||||
|   } | ||||
|  | ||||
|   while (this->available_()) { | ||||
|     uint8_t byte = this->read_byte_(); | ||||
|     if (this->parse_improv_serial_byte_(byte)) { | ||||
|       this->last_read_byte_ = now; | ||||
|     } else { | ||||
|       this->rx_buffer_.clear(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == improv::STATE_PROVISIONING) { | ||||
|     if (wifi::global_wifi_component->is_connected()) { | ||||
|       wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), | ||||
|                                                  this->connecting_sta_.get_password()); | ||||
|       this->connecting_sta_ = {}; | ||||
|       this->cancel_timeout("wifi-connect-timeout"); | ||||
|       this->set_state_(improv::STATE_PROVISIONED); | ||||
|  | ||||
|       std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); | ||||
|       this->send_response_(url); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { | ||||
|   std::vector<std::string> urls; | ||||
| #ifdef USE_WEBSERVER | ||||
|   auto ip = wifi::global_wifi_component->wifi_sta_ip(); | ||||
|   std::string webserver_url = "http://" + ip.str() + ":" + to_string(WEBSERVER_PORT); | ||||
|   urls.push_back(webserver_url); | ||||
| #endif | ||||
|   std::vector<uint8_t> data = improv::build_rpc_response(command, urls, false); | ||||
|   return data; | ||||
| } | ||||
|  | ||||
| std::vector<uint8_t> ImprovSerialComponent::build_version_info_() { | ||||
|   std::vector<std::string> infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()}; | ||||
|   std::vector<uint8_t> data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); | ||||
|   return data; | ||||
| }; | ||||
|  | ||||
| bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { | ||||
|   size_t at = this->rx_buffer_.size(); | ||||
|   this->rx_buffer_.push_back(byte); | ||||
|   ESP_LOGD(TAG, "Improv Serial byte: 0x%02X", byte); | ||||
|   const uint8_t *raw = &this->rx_buffer_[0]; | ||||
|   if (at == 0) | ||||
|     return byte == 'I'; | ||||
|   if (at == 1) | ||||
|     return byte == 'M'; | ||||
|   if (at == 2) | ||||
|     return byte == 'P'; | ||||
|   if (at == 3) | ||||
|     return byte == 'R'; | ||||
|   if (at == 4) | ||||
|     return byte == 'O'; | ||||
|   if (at == 5) | ||||
|     return byte == 'V'; | ||||
|  | ||||
|   if (at == 6) | ||||
|     return byte == IMPROV_SERIAL_VERSION; | ||||
|  | ||||
|   if (at == 7) | ||||
|     return true; | ||||
|   uint8_t type = raw[7]; | ||||
|  | ||||
|   if (at == 8) | ||||
|     return true; | ||||
|   uint8_t data_len = raw[8]; | ||||
|  | ||||
|   if (at < 8 + data_len) | ||||
|     return true; | ||||
|  | ||||
|   if (at == 8 + data_len) | ||||
|     return true; | ||||
|  | ||||
|   if (at == 8 + data_len + 1) { | ||||
|     uint8_t checksum = 0x00; | ||||
|     for (uint8_t i = 0; i < at; i++) | ||||
|       checksum += raw[i]; | ||||
|  | ||||
|     if (checksum != byte) { | ||||
|       ESP_LOGW(TAG, "Error decoding Improv payload"); | ||||
|       this->set_error_(improv::ERROR_INVALID_RPC); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     if (type == TYPE_RPC) { | ||||
|       this->set_error_(improv::ERROR_NONE); | ||||
|       auto command = improv::parse_improv_data(&raw[9], data_len, false); | ||||
|       return this->parse_improv_payload_(command); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // If we got here then the command coming is is improv, but not an RPC command | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) { | ||||
|   switch (command.command) { | ||||
|     case improv::WIFI_SETTINGS: { | ||||
|       wifi::WiFiAP sta{}; | ||||
|       sta.set_ssid(command.ssid); | ||||
|       sta.set_password(command.password); | ||||
|       this->connecting_sta_ = sta; | ||||
|  | ||||
|       wifi::global_wifi_component->set_sta(sta); | ||||
|       wifi::global_wifi_component->start_scanning(); | ||||
|       this->set_state_(improv::STATE_PROVISIONING); | ||||
|       ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), | ||||
|                command.password.c_str()); | ||||
|  | ||||
|       auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); | ||||
|       this->set_timeout("wifi-connect-timeout", 30000, f); | ||||
|       return true; | ||||
|     } | ||||
|     case improv::GET_CURRENT_STATE: | ||||
|       this->set_state_(this->state_); | ||||
|       if (this->state_ == improv::STATE_PROVISIONED) { | ||||
|         std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::GET_CURRENT_STATE); | ||||
|         this->send_response_(url); | ||||
|       } | ||||
|       return true; | ||||
|     case improv::GET_DEVICE_INFO: { | ||||
|       std::vector<uint8_t> info = this->build_version_info_(); | ||||
|       this->send_response_(info); | ||||
|       return true; | ||||
|     } | ||||
|     default: { | ||||
|       ESP_LOGW(TAG, "Unknown Improv payload"); | ||||
|       this->set_error_(improv::ERROR_UNKNOWN_RPC); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::set_state_(improv::State state) { | ||||
|   this->state_ = state; | ||||
|  | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(11); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_CURRENT_STATE; | ||||
|   data[8] = 1; | ||||
|   data[9] = state; | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data[10] = checksum; | ||||
|  | ||||
|   this->write_data_(data); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::set_error_(improv::Error error) { | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(11); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_ERROR_STATE; | ||||
|   data[8] = 1; | ||||
|   data[9] = error; | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data[10] = checksum; | ||||
|   this->write_data_(data); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) { | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(9); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_RPC_RESPONSE; | ||||
|   data[8] = response.size(); | ||||
|   data.insert(data.end(), response.begin(), response.end()); | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data.push_back(checksum); | ||||
|  | ||||
|   this->write_data_(data); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::on_wifi_connect_timeout_() { | ||||
|   this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); | ||||
|   this->set_state_(improv::STATE_AUTHORIZED); | ||||
|   ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); | ||||
|   wifi::global_wifi_component->clear_sta(); | ||||
| } | ||||
|  | ||||
| ImprovSerialComponent *global_improv_serial_component =  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|     nullptr;                                             // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace improv_serial | ||||
| }  // namespace esphome | ||||
							
								
								
									
										69
									
								
								esphome/components/improv_serial/improv_serial_component.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								esphome/components/improv_serial/improv_serial_component.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/improv/improv.h" | ||||
| #include "esphome/components/wifi/wifi_component.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
| #include <HardwareSerial.h> | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
| #include <driver/uart.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace improv_serial { | ||||
|  | ||||
| enum ImprovSerialType : uint8_t { | ||||
|   TYPE_CURRENT_STATE = 0x01, | ||||
|   TYPE_ERROR_STATE = 0x02, | ||||
|   TYPE_RPC = 0x03, | ||||
|   TYPE_RPC_RESPONSE = 0x04 | ||||
| }; | ||||
|  | ||||
| static const uint8_t IMPROV_SERIAL_VERSION = 1; | ||||
|  | ||||
| class ImprovSerialComponent : public Component { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|  protected: | ||||
|   bool parse_improv_serial_byte_(uint8_t byte); | ||||
|   bool parse_improv_payload_(improv::ImprovCommand &command); | ||||
|  | ||||
|   void set_state_(improv::State state); | ||||
|   void set_error_(improv::Error error); | ||||
|   void send_response_(std::vector<uint8_t> &response); | ||||
|   void on_wifi_connect_timeout_(); | ||||
|  | ||||
|   std::vector<uint8_t> build_rpc_settings_response_(improv::Command command); | ||||
|   std::vector<uint8_t> build_version_info_(); | ||||
|  | ||||
|   int available_(); | ||||
|   uint8_t read_byte_(); | ||||
|   void write_data_(std::vector<uint8_t> &data); | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
|   HardwareSerial *hw_serial_{nullptr}; | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
|   uart_port_t uart_num_; | ||||
| #endif | ||||
|  | ||||
|   std::vector<uint8_t> rx_buffer_; | ||||
|   uint32_t last_read_byte_{0}; | ||||
|   wifi::WiFiAP connecting_sta_; | ||||
|   improv::State state_{improv::STATE_AUTHORIZED}; | ||||
| }; | ||||
|  | ||||
| extern ImprovSerialComponent | ||||
|     *global_improv_serial_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace improv_serial | ||||
| }  // namespace esphome | ||||
| @@ -9,6 +9,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_BATTERY, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_PERCENT, | ||||
| @@ -53,6 +54,7 @@ CONFIG_SCHEMA = ( | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_BATTERY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|   | ||||
| @@ -87,7 +87,7 @@ class AddressableLight : public LightOutput, public Component { | ||||
|   void mark_shown_() { | ||||
| #ifdef USE_POWER_SUPPLY | ||||
|     for (const auto &c : *this) { | ||||
|       if (c.get().is_on()) { | ||||
|       if (c.get_red_raw() > 0 || c.get_green_raw() > 0 || c.get_blue_raw() > 0 || c.get_white_raw() > 0) { | ||||
|         this->power_.request(); | ||||
|         return; | ||||
|       } | ||||
|   | ||||
| @@ -221,7 +221,7 @@ UARTSelection Logger::get_uart() const { return this->uart_; } | ||||
| void Logger::add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback) { | ||||
|   this->log_callback_.add(std::move(callback)); | ||||
| } | ||||
| float Logger::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } | ||||
| float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } | ||||
| const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; | ||||
| #ifdef USE_ESP32 | ||||
| const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART2"}; | ||||
|   | ||||
| @@ -13,10 +13,20 @@ CONF_SCROLL_DELAY = "scroll_delay" | ||||
| CONF_SCROLL_ENABLE = "scroll_enable" | ||||
| CONF_SCROLL_MODE = "scroll_mode" | ||||
| CONF_REVERSE_ENABLE = "reverse_enable" | ||||
| CONF_NUM_CHIP_LINES = "num_chip_lines" | ||||
| CONF_CHIP_LINES_STYLE = "chip_lines_style" | ||||
|  | ||||
| integration_ns = cg.esphome_ns.namespace("max7219digit") | ||||
| ChipLinesStyle = integration_ns.enum("ChipLinesStyle") | ||||
| CHIP_LINES_STYLE = { | ||||
|     "ZIGZAG": ChipLinesStyle.ZIGZAG, | ||||
|     "SNAKE": ChipLinesStyle.SNAKE, | ||||
| } | ||||
|  | ||||
| ScrollMode = integration_ns.enum("ScrollMode") | ||||
| SCROLL_MODES = { | ||||
|     "CONTINUOUS": 0, | ||||
|     "STOP": 1, | ||||
|     "CONTINUOUS": ScrollMode.CONTINUOUS, | ||||
|     "STOP": ScrollMode.STOP, | ||||
| } | ||||
|  | ||||
| CHIP_MODES = { | ||||
| @@ -37,6 +47,10 @@ CONFIG_SCHEMA = ( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(MAX7219Component), | ||||
|             cv.Optional(CONF_NUM_CHIPS, default=4): cv.int_range(min=1, max=255), | ||||
|             cv.Optional(CONF_NUM_CHIP_LINES, default=1): cv.int_range(min=1, max=255), | ||||
|             cv.Optional(CONF_CHIP_LINES_STYLE, default="SNAKE"): cv.enum( | ||||
|                 CHIP_LINES_STYLE, upper=True | ||||
|             ), | ||||
|             cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), | ||||
|             cv.Optional(CONF_ROTATE_CHIP, default="0"): cv.enum(CHIP_MODES, upper=True), | ||||
|             cv.Optional(CONF_SCROLL_MODE, default="CONTINUOUS"): cv.enum( | ||||
| @@ -67,6 +81,8 @@ async def to_code(config): | ||||
|     await display.register_display(var, config) | ||||
|  | ||||
|     cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) | ||||
|     cg.add(var.set_num_chip_lines(config[CONF_NUM_CHIP_LINES])) | ||||
|     cg.add(var.set_chip_lines_style(config[CONF_CHIP_LINES_STYLE])) | ||||
|     cg.add(var.set_intensity(config[CONF_INTENSITY])) | ||||
|     cg.add(var.set_chip_orientation(config[CONF_ROTATE_CHIP])) | ||||
|     cg.add(var.set_scroll_speed(config[CONF_SCROLL_SPEED])) | ||||
|   | ||||
| @@ -26,9 +26,12 @@ void MAX7219Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up MAX7219_DIGITS..."); | ||||
|   this->spi_setup(); | ||||
|   this->stepsleft_ = 0; | ||||
|   this->max_displaybuffer_.reserve(500);  // Create base space to write buffer | ||||
|   // Initialize buffer with 0 for display so all non written pixels are blank | ||||
|   this->max_displaybuffer_.resize(this->num_chips_ * 8, 0); | ||||
|   for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { | ||||
|     std::vector<uint8_t> vec(1); | ||||
|     this->max_displaybuffer_.push_back(vec); | ||||
|     // Initialize buffer with 0 for display so all non written pixels are blank | ||||
|     this->max_displaybuffer_[chip_line].resize(get_width_internal(), 0); | ||||
|   } | ||||
|   // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway | ||||
|   this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); | ||||
|   // let's use our own ASCII -> led pattern encoding | ||||
| @@ -46,6 +49,8 @@ void MAX7219Component::setup() { | ||||
| void MAX7219Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "MAX7219DIGIT:"); | ||||
|   ESP_LOGCONFIG(TAG, "  Number of Chips: %u", this->num_chips_); | ||||
|   ESP_LOGCONFIG(TAG, "  Number of Chips Lines: %u", this->num_chip_lines_); | ||||
|   ESP_LOGCONFIG(TAG, "  Chips Lines Style : %u", this->chip_lines_style_); | ||||
|   ESP_LOGCONFIG(TAG, "  Intensity: %u", this->intensity_); | ||||
|   ESP_LOGCONFIG(TAG, "  Scroll Mode: %u", this->scroll_mode_); | ||||
|   ESP_LOGCONFIG(TAG, "  Scroll Speed: %u", this->scroll_speed_); | ||||
| @@ -59,19 +64,19 @@ void MAX7219Component::loop() { | ||||
|   uint32_t now = millis(); | ||||
|  | ||||
|   // check if the buffer has shrunk past the current position since last update | ||||
|   if ((this->max_displaybuffer_.size() >= this->old_buffer_size_ + 3) || | ||||
|       (this->max_displaybuffer_.size() <= this->old_buffer_size_ - 3)) { | ||||
|   if ((this->max_displaybuffer_[0].size() >= this->old_buffer_size_ + 3) || | ||||
|       (this->max_displaybuffer_[0].size() <= this->old_buffer_size_ - 3)) { | ||||
|     this->stepsleft_ = 0; | ||||
|     this->display(); | ||||
|     this->old_buffer_size_ = this->max_displaybuffer_.size(); | ||||
|     this->old_buffer_size_ = this->max_displaybuffer_[0].size(); | ||||
|   } | ||||
|  | ||||
|   // Reset the counter back to 0 when full string has been displayed. | ||||
|   if (this->stepsleft_ > this->max_displaybuffer_.size()) | ||||
|   if (this->stepsleft_ > this->max_displaybuffer_[0].size()) | ||||
|     this->stepsleft_ = 0; | ||||
|  | ||||
|   // Return if there is no need to scroll or scroll is off | ||||
|   if (!this->scroll_ || (this->max_displaybuffer_.size() <= this->num_chips_ * 8)) { | ||||
|   if (!this->scroll_ || (this->max_displaybuffer_[0].size() <= get_width_internal())) { | ||||
|     this->display(); | ||||
|     return; | ||||
|   } | ||||
| @@ -82,8 +87,8 @@ void MAX7219Component::loop() { | ||||
|   } | ||||
|  | ||||
|   // Dwell time at end of string in case of stop at end | ||||
|   if (this->scroll_mode_ == 1) { | ||||
|     if (this->stepsleft_ >= this->max_displaybuffer_.size() - this->num_chips_ * 8 + 1) { | ||||
|   if (this->scroll_mode_ == ScrollMode::STOP) { | ||||
|     if (this->stepsleft_ >= this->max_displaybuffer_[0].size() - get_width_internal() + 1) { | ||||
|       if (now - this->last_scroll_ >= this->scroll_dwell_) { | ||||
|         this->stepsleft_ = 0; | ||||
|         this->last_scroll_ = now; | ||||
| @@ -107,30 +112,53 @@ void MAX7219Component::display() { | ||||
|   // Run this routine for the rows of every chip 8x row 0 top to 7 bottom | ||||
|   // Fill the pixel parameter with display data | ||||
|   // Send the data to the chip | ||||
|   for (uint8_t i = 0; i < this->num_chips_; i++) { | ||||
|     for (uint8_t j = 0; j < 8; j++) { | ||||
|       if (this->reverse_) { | ||||
|         pixels[j] = this->max_displaybuffer_[(this->num_chips_ - i - 1) * 8 + j]; | ||||
|       } else { | ||||
|         pixels[j] = this->max_displaybuffer_[i * 8 + j]; | ||||
|   for (uint8_t chip = 0; chip < this->num_chips_ / this->num_chip_lines_; chip++) { | ||||
|     for (uint8_t chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { | ||||
|       for (uint8_t j = 0; j < 8; j++) { | ||||
|         bool reverse = | ||||
|             chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE ? !this->reverse_ : this->reverse_; | ||||
|         if (reverse) { | ||||
|           pixels[j] = | ||||
|               this->max_displaybuffer_[chip_line][(this->num_chips_ / this->num_chip_lines_ - chip - 1) * 8 + j]; | ||||
|         } else { | ||||
|           pixels[j] = this->max_displaybuffer_[chip_line][chip * 8 + j]; | ||||
|         } | ||||
|       } | ||||
|       if (chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE) | ||||
|         this->orientation_ = orientation_180_(); | ||||
|       this->send64pixels(chip_line * this->num_chips_ / this->num_chip_lines_ + chip, pixels); | ||||
|       if (chip_line % 2 != 0 && this->chip_lines_style_ == ChipLinesStyle::SNAKE) | ||||
|         this->orientation_ = orientation_180_(); | ||||
|     } | ||||
|     this->send64pixels(i, pixels); | ||||
|   } | ||||
| } | ||||
|  | ||||
| uint8_t MAX7219Component::orientation_180_() { | ||||
|   switch (this->orientation_) { | ||||
|     case 0: | ||||
|       return 2; | ||||
|     case 1: | ||||
|       return 3; | ||||
|     case 2: | ||||
|       return 0; | ||||
|     case 3: | ||||
|       return 1; | ||||
|     default: | ||||
|       return 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| int MAX7219Component::get_height_internal() { | ||||
|   return 8;  // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHER | ||||
|              // TO BE DONE -> CREATE Virtual size of screen and scroll | ||||
|   return 8 * this->num_chip_lines_;  // TO BE DONE -> CREATE Virtual size of screen and scroll | ||||
| } | ||||
|  | ||||
| int MAX7219Component::get_width_internal() { return this->num_chips_ * 8; } | ||||
|  | ||||
| size_t MAX7219Component::get_buffer_length_() { return this->num_chips_ * 8; } | ||||
| int MAX7219Component::get_width_internal() { return this->num_chips_ / this->num_chip_lines_ * 8; } | ||||
|  | ||||
| void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color color) { | ||||
|   if (x + 1 > this->max_displaybuffer_.size()) {  // Extend the display buffer in case required | ||||
|     this->max_displaybuffer_.resize(x + 1, this->bckgrnd_); | ||||
|   if (x + 1 > this->max_displaybuffer_[0].size()) {  // Extend the display buffer in case required | ||||
|     for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { | ||||
|       this->max_displaybuffer_[chip_line].resize(x + 1, this->bckgrnd_); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if ((y >= this->get_height_internal()) || (y < 0) || (x < 0))  // If pixel is outside display then dont draw | ||||
| @@ -140,9 +168,9 @@ void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color colo | ||||
|   uint8_t subpos = y;  // Y is starting at 0 top left | ||||
|  | ||||
|   if (color.is_on()) { | ||||
|     this->max_displaybuffer_[pos] |= (1 << subpos); | ||||
|     this->max_displaybuffer_[subpos / 8][pos] |= (1 << subpos % 8); | ||||
|   } else { | ||||
|     this->max_displaybuffer_[pos] &= ~(1 << subpos); | ||||
|     this->max_displaybuffer_[subpos / 8][pos] &= ~(1 << subpos % 8); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -158,8 +186,10 @@ void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { | ||||
| } | ||||
| void MAX7219Component::update() { | ||||
|   this->update_ = true; | ||||
|   this->max_displaybuffer_.clear(); | ||||
|   this->max_displaybuffer_.resize(this->num_chips_ * 8, this->bckgrnd_); | ||||
|   for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { | ||||
|     this->max_displaybuffer_[chip_line].clear(); | ||||
|     this->max_displaybuffer_[chip_line].resize(get_width_internal(), this->bckgrnd_); | ||||
|   } | ||||
|   if (this->writer_local_.has_value())  // insert Labda function if available | ||||
|     (*this->writer_local_)(*this); | ||||
| } | ||||
| @@ -175,7 +205,7 @@ void MAX7219Component::turn_on_off(bool on_off) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MAX7219Component::scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell) { | ||||
| void MAX7219Component::scroll(bool on_off, ScrollMode mode, uint16_t speed, uint16_t delay, uint16_t dwell) { | ||||
|   this->set_scroll(on_off); | ||||
|   this->set_scroll_mode(mode); | ||||
|   this->set_scroll_speed(speed); | ||||
| @@ -183,7 +213,7 @@ void MAX7219Component::scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_ | ||||
|   this->set_scroll_delay(delay); | ||||
| } | ||||
|  | ||||
| void MAX7219Component::scroll(bool on_off, uint8_t mode) { | ||||
| void MAX7219Component::scroll(bool on_off, ScrollMode mode) { | ||||
|   this->set_scroll(on_off); | ||||
|   this->set_scroll_mode(mode); | ||||
| } | ||||
| @@ -196,24 +226,26 @@ void MAX7219Component::intensity(uint8_t intensity) { | ||||
| void MAX7219Component::scroll(bool on_off) { this->set_scroll(on_off); } | ||||
|  | ||||
| void MAX7219Component::scroll_left() { | ||||
|   if (this->update_) { | ||||
|     this->max_displaybuffer_.push_back(this->bckgrnd_); | ||||
|     for (uint16_t i = 0; i < this->stepsleft_; i++) { | ||||
|       this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); | ||||
|       this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); | ||||
|       this->update_ = false; | ||||
|   for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { | ||||
|     if (this->update_) { | ||||
|       this->max_displaybuffer_[chip_line].push_back(this->bckgrnd_); | ||||
|       for (uint16_t i = 0; i < this->stepsleft_; i++) { | ||||
|         this->max_displaybuffer_[chip_line].push_back(this->max_displaybuffer_[chip_line].front()); | ||||
|         this->max_displaybuffer_[chip_line].erase(this->max_displaybuffer_[chip_line].begin()); | ||||
|       } | ||||
|     } else { | ||||
|       this->max_displaybuffer_[chip_line].push_back(this->max_displaybuffer_[chip_line].front()); | ||||
|       this->max_displaybuffer_[chip_line].erase(this->max_displaybuffer_[chip_line].begin()); | ||||
|     } | ||||
|   } else { | ||||
|     this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); | ||||
|     this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); | ||||
|   } | ||||
|   this->update_ = false; | ||||
|   this->stepsleft_++; | ||||
| } | ||||
|  | ||||
| void MAX7219Component::send_char(uint8_t chip, uint8_t data) { | ||||
|   // get this character from PROGMEM | ||||
|   for (uint8_t i = 0; i < 8; i++) | ||||
|     this->max_displaybuffer_[chip * 8 + i] = progmem_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); | ||||
|     this->max_displaybuffer_[0][chip * 8 + i] = progmem_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); | ||||
| }  // end of send_char | ||||
|  | ||||
| // send one character (data) to position (chip) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user