mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 06:33:51 +00:00 
			
		
		
		
	Merge branch 'esphome:dev' into gsm
This commit is contained in:
		
							
								
								
									
										4
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -46,7 +46,7 @@ runs: | ||||
|  | ||||
|     - name: Build and push to ghcr by digest | ||||
|       id: build-ghcr | ||||
|       uses: docker/build-push-action@v6.3.0 | ||||
|       uses: docker/build-push-action@v6.5.0 | ||||
|       with: | ||||
|         context: . | ||||
|         file: ./docker/Dockerfile | ||||
| @@ -69,7 +69,7 @@ runs: | ||||
|  | ||||
|     - name: Build and push to dockerhub by digest | ||||
|       id: build-dockerhub | ||||
|       uses: docker/build-push-action@v6.3.0 | ||||
|       uses: docker/build-push-action@v6.5.0 | ||||
|       with: | ||||
|         context: . | ||||
|         file: ./docker/Dockerfile | ||||
|   | ||||
							
								
								
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,6 +13,13 @@ updates: | ||||
|     schedule: | ||||
|       interval: daily | ||||
|     open-pull-requests-limit: 10 | ||||
|     groups: | ||||
|       docker-actions: | ||||
|         applies-to: version-updates | ||||
|         patterns: | ||||
|           - "docker/setup-qemu-action" | ||||
|           - "docker/login-action" | ||||
|           - "docker/setup-buildx-action" | ||||
|   - package-ecosystem: github-actions | ||||
|     directory: "/.github/actions/build-image" | ||||
|     schedule: | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -46,9 +46,9 @@ jobs: | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.4.0 | ||||
|         uses: docker/setup-buildx-action@v3.5.0 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v3.1.0 | ||||
|         uses: docker/setup-qemu-action@v3.2.0 | ||||
|  | ||||
|       - name: Set TAG | ||||
|         run: | | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -468,6 +468,8 @@ jobs: | ||||
|       - name: Compile config | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           mkdir build_cache | ||||
|           export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache | ||||
|           for component in ${{ matrix.components }}; do | ||||
|             ./script/test_build_components -e compile -c $component | ||||
|           done | ||||
|   | ||||
							
								
								
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -90,18 +90,18 @@ jobs: | ||||
|           python-version: "3.9" | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.4.0 | ||||
|         uses: docker/setup-buildx-action@v3.5.0 | ||||
|       - name: Set up QEMU | ||||
|         if: matrix.platform != 'linux/amd64' | ||||
|         uses: docker/setup-qemu-action@v3.1.0 | ||||
|         uses: docker/setup-qemu-action@v3.2.0 | ||||
|  | ||||
|       - name: Log in to docker hub | ||||
|         uses: docker/login-action@v3.2.0 | ||||
|         uses: docker/login-action@v3.3.0 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKER_USER }} | ||||
|           password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|       - name: Log in to the GitHub container registry | ||||
|         uses: docker/login-action@v3.2.0 | ||||
|         uses: docker/login-action@v3.3.0 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
| @@ -184,17 +184,17 @@ jobs: | ||||
|           merge-multiple: true | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.4.0 | ||||
|         uses: docker/setup-buildx-action@v3.5.0 | ||||
|  | ||||
|       - name: Log in to docker hub | ||||
|         if: matrix.registry == 'dockerhub' | ||||
|         uses: docker/login-action@v3.2.0 | ||||
|         uses: docker/login-action@v3.3.0 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKER_USER }} | ||||
|           password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|       - name: Log in to the GitHub container registry | ||||
|         if: matrix.registry == 'ghcr' | ||||
|         uses: docker/login-action@v3.2.0 | ||||
|         uses: docker/login-action@v3.3.0 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|   | ||||
| @@ -2,6 +2,15 @@ | ||||
| # See https://pre-commit.com for more information | ||||
| # See https://pre-commit.com/hooks.html for more hooks | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.5.4 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|         args: [--fix] | ||||
|       # Run the formatter. | ||||
|       - id: ruff-format | ||||
|   - repo: https://github.com/psf/black-pre-commit-mirror | ||||
|     rev: 24.4.2 | ||||
|     hooks: | ||||
|   | ||||
| @@ -37,6 +37,7 @@ esphome/components/am43/sensor/* @buxtronix | ||||
| esphome/components/analog_threshold/* @ianchi | ||||
| esphome/components/animation/* @syndlex | ||||
| esphome/components/anova/* @buxtronix | ||||
| esphome/components/apds9306/* @aodrenah | ||||
| esphome/components/api/* @OttoWinter | ||||
| esphome/components/as5600/* @ammmze | ||||
| esphome/components/as5600/sensor/* @ammmze | ||||
| @@ -216,6 +217,8 @@ esphome/components/lock/* @esphome/core | ||||
| esphome/components/logger/* @esphome/core | ||||
| esphome/components/ltr390/* @latonita @sjtrny | ||||
| esphome/components/ltr_als_ps/* @latonita | ||||
| esphome/components/lvgl/* @clydebarrow | ||||
| esphome/components/m5stack_8angle/* @rnauber | ||||
| esphome/components/matrix_keypad/* @ssieb | ||||
| esphome/components/max31865/* @DAVe3283 | ||||
| esphome/components/max44009/* @berfenger | ||||
|   | ||||
| @@ -695,7 +695,8 @@ def command_rename(args, config): | ||||
|         os.remove(new_path) | ||||
|         return 1 | ||||
|  | ||||
|     os.remove(CORE.config_path) | ||||
|     if CORE.config_path != new_path: | ||||
|         os.remove(CORE.config_path) | ||||
|  | ||||
|     print(color(Fore.BOLD_GREEN, "SUCCESS")) | ||||
|     print() | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( | ||||
| CONFIG_SCHEMA = cv.invalid( | ||||
|     "The ade7953 sensor component has been renamed to ade7953_i2c." | ||||
| ) | ||||
|   | ||||
							
								
								
									
										4
									
								
								esphome/components/apds9306/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								esphome/components/apds9306/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| # Based on this datasheet: | ||||
| # https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf | ||||
|  | ||||
| CODEOWNERS = ["@aodrenah"] | ||||
							
								
								
									
										151
									
								
								esphome/components/apds9306/apds9306.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								esphome/components/apds9306/apds9306.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| // Based on this datasheet: | ||||
| // https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf | ||||
|  | ||||
| #include "apds9306.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace apds9306 { | ||||
|  | ||||
| static const char *const TAG = "apds9306"; | ||||
|  | ||||
| enum {  // APDS9306 registers | ||||
|   APDS9306_MAIN_CTRL = 0x00, | ||||
|   APDS9306_ALS_MEAS_RATE = 0x04, | ||||
|   APDS9306_ALS_GAIN = 0x05, | ||||
|   APDS9306_PART_ID = 0x06, | ||||
|   APDS9306_MAIN_STATUS = 0x07, | ||||
|   APDS9306_CLEAR_DATA_0 = 0x0A,  // LSB | ||||
|   APDS9306_CLEAR_DATA_1 = 0x0B, | ||||
|   APDS9306_CLEAR_DATA_2 = 0x0C,  // MSB | ||||
|   APDS9306_ALS_DATA_0 = 0x0D,    // LSB | ||||
|   APDS9306_ALS_DATA_1 = 0x0E, | ||||
|   APDS9306_ALS_DATA_2 = 0x0F,  // MSB | ||||
|   APDS9306_INT_CFG = 0x19, | ||||
|   APDS9306_INT_PERSISTENCE = 0x1A, | ||||
|   APDS9306_ALS_THRES_UP_0 = 0x21,  // LSB | ||||
|   APDS9306_ALS_THRES_UP_1 = 0x22, | ||||
|   APDS9306_ALS_THRES_UP_2 = 0x23,   // MSB | ||||
|   APDS9306_ALS_THRES_LOW_0 = 0x24,  // LSB | ||||
|   APDS9306_ALS_THRES_LOW_1 = 0x25, | ||||
|   APDS9306_ALS_THRES_LOW_2 = 0x26,  // MSB | ||||
|   APDS9306_ALS_THRES_VAR = 0x27 | ||||
| }; | ||||
|  | ||||
| #define APDS9306_ERROR_CHECK(func, error) \ | ||||
|   if (!(func)) { \ | ||||
|     ESP_LOGE(TAG, error); \ | ||||
|     this->mark_failed(); \ | ||||
|     return; \ | ||||
|   } | ||||
| #define APDS9306_WARNING_CHECK(func, warning) \ | ||||
|   if (!(func)) { \ | ||||
|     ESP_LOGW(TAG, warning); \ | ||||
|     this->status_set_warning(); \ | ||||
|     return; \ | ||||
|   } | ||||
| #define APDS9306_WRITE_BYTE(reg, value) \ | ||||
|   ESP_LOGV(TAG, "Writing 0x%02x to 0x%02x", value, reg); \ | ||||
|   if (!this->write_byte(reg, value)) { \ | ||||
|     ESP_LOGE(TAG, "Failed writing 0x%02x to 0x%02x", value, reg); \ | ||||
|     this->mark_failed(); \ | ||||
|     return; \ | ||||
|   } | ||||
|  | ||||
| void APDS9306::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up APDS9306..."); | ||||
|  | ||||
|   uint8_t id; | ||||
|   if (!this->read_byte(APDS9306_PART_ID, &id)) {  // Part ID register | ||||
|     this->error_code_ = COMMUNICATION_FAILED; | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (id != 0xB1 && id != 0xB3) {  // 0xB1 for APDS9306 0xB3 for APDS9306-065 | ||||
|     this->error_code_ = WRONG_ID; | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // ALS resolution and measurement, see datasheet or init.py for options | ||||
|   uint8_t als_meas_rate = ((this->bit_width_ & 0x07) << 4) | (this->measurement_rate_ & 0x07); | ||||
|   APDS9306_WRITE_BYTE(APDS9306_ALS_MEAS_RATE, als_meas_rate); | ||||
|  | ||||
|   // ALS gain, see datasheet or init.py for options | ||||
|   uint8_t als_gain = (this->gain_ & 0x07); | ||||
|   APDS9306_WRITE_BYTE(APDS9306_ALS_GAIN, als_gain); | ||||
|  | ||||
|   // Set to standby mode | ||||
|   APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00); | ||||
|  | ||||
|   // Check for data, clear main status | ||||
|   uint8_t status; | ||||
|   APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed."); | ||||
|  | ||||
|   // Set to active mode | ||||
|   APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02); | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "APDS9306 setup complete"); | ||||
| } | ||||
|  | ||||
| void APDS9306::dump_config() { | ||||
|   LOG_SENSOR("", "APDS9306", this); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|  | ||||
|   if (this->is_failed()) { | ||||
|     switch (this->error_code_) { | ||||
|       case COMMUNICATION_FAILED: | ||||
|         ESP_LOGE(TAG, "Communication with APDS9306 failed!"); | ||||
|         break; | ||||
|       case WRONG_ID: | ||||
|         ESP_LOGE(TAG, "APDS9306 has invalid id!"); | ||||
|         break; | ||||
|       default: | ||||
|         ESP_LOGE(TAG, "Setting up APDS9306 registers failed!"); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Gain: %u", AMBIENT_LIGHT_GAIN_VALUES[this->gain_]); | ||||
|   ESP_LOGCONFIG(TAG, "  Measurement rate: %u", MEASUREMENT_RATE_VALUES[this->measurement_rate_]); | ||||
|   ESP_LOGCONFIG(TAG, "  Measurement Resolution/Bit width: %d", MEASUREMENT_BIT_WIDTH_VALUES[this->bit_width_]); | ||||
|  | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| void APDS9306::update() { | ||||
|   // Check for new data | ||||
|   uint8_t status; | ||||
|   APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed."); | ||||
|  | ||||
|   this->status_clear_warning(); | ||||
|  | ||||
|   if (!(status &= 0b00001000)) {  // No new data | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Set to standby mode | ||||
|   APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00); | ||||
|  | ||||
|   // Clear MAIN STATUS | ||||
|   APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed."); | ||||
|  | ||||
|   uint8_t als_data[3]; | ||||
|   APDS9306_WARNING_CHECK(this->read_bytes(APDS9306_ALS_DATA_0, als_data, 3), "Reading ALS data has failed."); | ||||
|  | ||||
|   // Set to active mode | ||||
|   APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02); | ||||
|  | ||||
|   uint32_t light_level = 0x00 | encode_uint24(als_data[2], als_data[1], als_data[0]); | ||||
|  | ||||
|   float lux = ((float) light_level / AMBIENT_LIGHT_GAIN_VALUES[this->gain_]) * | ||||
|               (100.0f / MEASUREMENT_RATE_VALUES[this->measurement_rate_]); | ||||
|  | ||||
|   ESP_LOGD(TAG, "Got illuminance=%.1flx from", lux); | ||||
|   this->publish_state(lux); | ||||
| } | ||||
|  | ||||
| }  // namespace apds9306 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										66
									
								
								esphome/components/apds9306/apds9306.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								esphome/components/apds9306/apds9306.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| // Based on this datasheet: | ||||
| // https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace apds9306 { | ||||
|  | ||||
| enum MeasurementBitWidth : uint8_t { | ||||
|   MEASUREMENT_BIT_WIDTH_20 = 0, | ||||
|   MEASUREMENT_BIT_WIDTH_19 = 1, | ||||
|   MEASUREMENT_BIT_WIDTH_18 = 2, | ||||
|   MEASUREMENT_BIT_WIDTH_17 = 3, | ||||
|   MEASUREMENT_BIT_WIDTH_16 = 4, | ||||
|   MEASUREMENT_BIT_WIDTH_13 = 5, | ||||
| }; | ||||
| static const uint8_t MEASUREMENT_BIT_WIDTH_VALUES[] = {20, 19, 18, 17, 16, 13}; | ||||
|  | ||||
| enum MeasurementRate : uint8_t { | ||||
|   MEASUREMENT_RATE_25 = 0, | ||||
|   MEASUREMENT_RATE_50 = 1, | ||||
|   MEASUREMENT_RATE_100 = 2, | ||||
|   MEASUREMENT_RATE_200 = 3, | ||||
|   MEASUREMENT_RATE_500 = 4, | ||||
|   MEASUREMENT_RATE_1000 = 5, | ||||
|   MEASUREMENT_RATE_2000 = 6, | ||||
| }; | ||||
| static const uint16_t MEASUREMENT_RATE_VALUES[] = {25, 50, 100, 200, 500, 1000, 2000}; | ||||
|  | ||||
| enum AmbientLightGain : uint8_t { | ||||
|   AMBIENT_LIGHT_GAIN_1 = 0, | ||||
|   AMBIENT_LIGHT_GAIN_3 = 1, | ||||
|   AMBIENT_LIGHT_GAIN_6 = 2, | ||||
|   AMBIENT_LIGHT_GAIN_9 = 3, | ||||
|   AMBIENT_LIGHT_GAIN_18 = 4, | ||||
| }; | ||||
| static const uint8_t AMBIENT_LIGHT_GAIN_VALUES[] = {1, 3, 6, 9, 18}; | ||||
|  | ||||
| class APDS9306 : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   float get_setup_priority() const override { return setup_priority::BUS; } | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|   void set_bit_width(MeasurementBitWidth bit_width) { this->bit_width_ = bit_width; } | ||||
|   void set_measurement_rate(MeasurementRate measurement_rate) { this->measurement_rate_ = measurement_rate; } | ||||
|   void set_ambient_light_gain(AmbientLightGain gain) { this->gain_ = gain; } | ||||
|  | ||||
|  protected: | ||||
|   enum ErrorCode { | ||||
|     NONE = 0, | ||||
|     COMMUNICATION_FAILED, | ||||
|     WRONG_ID, | ||||
|   } error_code_{NONE}; | ||||
|  | ||||
|   MeasurementBitWidth bit_width_; | ||||
|   MeasurementRate measurement_rate_; | ||||
|   AmbientLightGain gain_; | ||||
| }; | ||||
|  | ||||
| }  // namespace apds9306 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										95
									
								
								esphome/components/apds9306/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								esphome/components/apds9306/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| # Based on this datasheet: | ||||
| # https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c, sensor | ||||
| from esphome.const import ( | ||||
|     CONF_GAIN, | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
|     ICON_LIGHTBULB, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_LUX, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| CONF_APDS9306_ID = "apds9306_id" | ||||
| CONF_BIT_WIDTH = "bit_width" | ||||
| CONF_MEASUREMENT_RATE = "measurement_rate" | ||||
|  | ||||
| apds9306_ns = cg.esphome_ns.namespace("apds9306") | ||||
| APDS9306 = apds9306_ns.class_( | ||||
|     "APDS9306", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| MeasurementBitWidth = apds9306_ns.enum("MeasurementBitWidth") | ||||
| MeasurementRate = apds9306_ns.enum("MeasurementRate") | ||||
| AmbientLightGain = apds9306_ns.enum("AmbientLightGain") | ||||
|  | ||||
| MEASUREMENT_BIT_WIDTHS = { | ||||
|     20: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_20, | ||||
|     19: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_19, | ||||
|     18: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_18, | ||||
|     17: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_17, | ||||
|     16: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_16, | ||||
|     13: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_13, | ||||
| } | ||||
|  | ||||
| MEASUREMENT_RATES = { | ||||
|     25: MeasurementRate.MEASUREMENT_RATE_25, | ||||
|     50: MeasurementRate.MEASUREMENT_RATE_50, | ||||
|     100: MeasurementRate.MEASUREMENT_RATE_100, | ||||
|     200: MeasurementRate.MEASUREMENT_RATE_200, | ||||
|     500: MeasurementRate.MEASUREMENT_RATE_500, | ||||
|     1000: MeasurementRate.MEASUREMENT_RATE_1000, | ||||
|     2000: MeasurementRate.MEASUREMENT_RATE_2000, | ||||
| } | ||||
|  | ||||
| AMBIENT_LIGHT_GAINS = { | ||||
|     1: AmbientLightGain.AMBIENT_LIGHT_GAIN_1, | ||||
|     3: AmbientLightGain.AMBIENT_LIGHT_GAIN_3, | ||||
|     6: AmbientLightGain.AMBIENT_LIGHT_GAIN_6, | ||||
|     9: AmbientLightGain.AMBIENT_LIGHT_GAIN_9, | ||||
|     18: AmbientLightGain.AMBIENT_LIGHT_GAIN_18, | ||||
| } | ||||
|  | ||||
|  | ||||
| def _validate_measurement_rate(value): | ||||
|     value = cv.positive_time_period_milliseconds(value) | ||||
|     return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         APDS9306, | ||||
|         unit_of_measurement=UNIT_LUX, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|         icon=ICON_LIGHTBULB, | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Optional(CONF_GAIN, default="1"): cv.enum(AMBIENT_LIGHT_GAINS, int=True), | ||||
|             cv.Optional(CONF_BIT_WIDTH, default="18"): cv.enum( | ||||
|                 MEASUREMENT_BIT_WIDTHS, int=True | ||||
|             ), | ||||
|             cv.Optional( | ||||
|                 CONF_MEASUREMENT_RATE, default="100ms" | ||||
|             ): _validate_measurement_rate, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x52)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     cg.add(var.set_bit_width(config[CONF_BIT_WIDTH])) | ||||
|     cg.add(var.set_measurement_rate(config[CONF_MEASUREMENT_RATE])) | ||||
|     cg.add(var.set_ambient_light_gain(config[CONF_GAIN])) | ||||
| @@ -2,6 +2,6 @@ import esphome.config_validation as cv | ||||
|  | ||||
| CODEOWNERS = ["@latonita"] | ||||
|  | ||||
| CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( | ||||
| CONFIG_SCHEMA = cv.invalid( | ||||
|     "The bmp3xx sensor component has been renamed to bmp3xx_i2c." | ||||
| ) | ||||
|   | ||||
| @@ -2,6 +2,6 @@ import esphome.config_validation as cv | ||||
|  | ||||
| CODEOWNERS = ["@latonita"] | ||||
|  | ||||
| CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( | ||||
| CONFIG_SCHEMA = cv.invalid( | ||||
|     "The ens160 sensor component has been renamed to ens160_i2c." | ||||
| ) | ||||
|   | ||||
| @@ -7,10 +7,10 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant | ||||
|  | ||||
| DEPENDENCIES = ["esp32"] | ||||
| CODEOWNERS = ["@jesserockz", "@Rapsssito"] | ||||
| CONFLICTS_WITH = ["esp32_ble_beacon"] | ||||
|  | ||||
| CONF_BLE_ID = "ble_id" | ||||
| CONF_IO_CAPABILITY = "io_capability" | ||||
| CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" | ||||
|  | ||||
| NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] | ||||
|  | ||||
| @@ -34,6 +34,19 @@ IO_CAPABILITY = { | ||||
|     "display_yes_no": IoCapability.IO_CAP_IO, | ||||
| } | ||||
|  | ||||
| esp_power_level_t = cg.global_ns.enum("esp_power_level_t") | ||||
|  | ||||
| TX_POWER_LEVELS = { | ||||
|     -12: esp_power_level_t.ESP_PWR_LVL_N12, | ||||
|     -9: esp_power_level_t.ESP_PWR_LVL_N9, | ||||
|     -6: esp_power_level_t.ESP_PWR_LVL_N6, | ||||
|     -3: esp_power_level_t.ESP_PWR_LVL_N3, | ||||
|     0: esp_power_level_t.ESP_PWR_LVL_N0, | ||||
|     3: esp_power_level_t.ESP_PWR_LVL_P3, | ||||
|     6: esp_power_level_t.ESP_PWR_LVL_P6, | ||||
|     9: esp_power_level_t.ESP_PWR_LVL_P9, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(ESP32BLE), | ||||
| @@ -41,6 +54,9 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|             IO_CAPABILITY, lower=True | ||||
|         ), | ||||
|         cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, | ||||
|         cv.Optional( | ||||
|             CONF_ADVERTISING_CYCLE_TIME, default="10s" | ||||
|         ): cv.positive_time_period_milliseconds, | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
| @@ -58,6 +74,7 @@ async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) | ||||
|     cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) | ||||
|     cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME])) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     if CORE.using_esp_idf: | ||||
|   | ||||
| @@ -78,6 +78,11 @@ void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &dat | ||||
|   this->advertising_start(); | ||||
| } | ||||
|  | ||||
| void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) { | ||||
|   this->advertising_init_(); | ||||
|   this->advertising_->register_raw_advertisement_callback(std::move(callback)); | ||||
| } | ||||
|  | ||||
| void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) { | ||||
|   this->advertising_init_(); | ||||
|   this->advertising_->add_service_uuid(uuid); | ||||
| @@ -102,7 +107,7 @@ bool ESP32BLE::ble_pre_setup_() { | ||||
| void ESP32BLE::advertising_init_() { | ||||
|   if (this->advertising_ != nullptr) | ||||
|     return; | ||||
|   this->advertising_ = new BLEAdvertising();  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|   this->advertising_ = new BLEAdvertising(this->advertising_cycle_time_);  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|  | ||||
|   this->advertising_->set_scan_response(true); | ||||
|   this->advertising_->set_min_preferred_interval(0x06); | ||||
| @@ -312,6 +317,9 @@ void ESP32BLE::loop() { | ||||
|     delete ble_event;  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|     ble_event = this->ble_events_.pop(); | ||||
|   } | ||||
|   if (this->advertising_ != nullptr) { | ||||
|     this->advertising_->loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { | ||||
|   | ||||
| @@ -3,6 +3,8 @@ | ||||
| #include "ble_advertising.h" | ||||
| #include "ble_uuid.h" | ||||
|  | ||||
| #include <functional> | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| @@ -76,6 +78,11 @@ class ESP32BLE : public Component { | ||||
|  public: | ||||
|   void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } | ||||
|  | ||||
|   void set_advertising_cycle_time(uint32_t advertising_cycle_time) { | ||||
|     this->advertising_cycle_time_ = advertising_cycle_time; | ||||
|   } | ||||
|   uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; } | ||||
|  | ||||
|   void enable(); | ||||
|   void disable(); | ||||
|   bool is_active(); | ||||
| @@ -89,6 +96,7 @@ class ESP32BLE : public Component { | ||||
|   void advertising_set_manufacturer_data(const std::vector<uint8_t> &data); | ||||
|   void advertising_add_service_uuid(ESPBTUUID uuid); | ||||
|   void advertising_remove_service_uuid(ESPBTUUID uuid); | ||||
|   void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback); | ||||
|  | ||||
|   void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } | ||||
|   void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } | ||||
| @@ -121,6 +129,7 @@ class ESP32BLE : public Component { | ||||
|   Queue<BLEEvent> ble_events_; | ||||
|   BLEAdvertising *advertising_; | ||||
|   esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; | ||||
|   uint32_t advertising_cycle_time_; | ||||
|   bool enable_on_boot_; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -10,9 +10,9 @@ | ||||
| namespace esphome { | ||||
| namespace esp32_ble { | ||||
|  | ||||
| static const char *const TAG = "esp32_ble"; | ||||
| static const char *const TAG = "esp32_ble.advertising"; | ||||
|  | ||||
| BLEAdvertising::BLEAdvertising() { | ||||
| BLEAdvertising::BLEAdvertising(uint32_t advertising_cycle_time) : advertising_cycle_time_(advertising_cycle_time) { | ||||
|   this->advertising_data_.set_scan_rsp = false; | ||||
|   this->advertising_data_.include_name = true; | ||||
|   this->advertising_data_.include_txpower = true; | ||||
| @@ -64,7 +64,7 @@ void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLEAdvertising::start() { | ||||
| esp_err_t BLEAdvertising::services_advertisement_() { | ||||
|   int num_services = this->advertising_uuids_.size(); | ||||
|   if (num_services == 0) { | ||||
|     this->advertising_data_.service_uuid_len = 0; | ||||
| @@ -87,8 +87,8 @@ void BLEAdvertising::start() { | ||||
|   this->advertising_data_.include_txpower = !this->scan_response_; | ||||
|   err = esp_ble_gap_config_adv_data(&this->advertising_data_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %d", err); | ||||
|     return; | ||||
|     ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %s", esp_err_to_name(err)); | ||||
|     return err; | ||||
|   } | ||||
|  | ||||
|   if (this->scan_response_) { | ||||
| @@ -101,8 +101,8 @@ void BLEAdvertising::start() { | ||||
|     this->scan_response_data_.flag = 0; | ||||
|     err = esp_ble_gap_config_adv_data(&this->scan_response_data_); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %d", err); | ||||
|       return; | ||||
|       ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %s", esp_err_to_name(err)); | ||||
|       return err; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -113,8 +113,18 @@ void BLEAdvertising::start() { | ||||
|  | ||||
|   err = esp_ble_gap_start_advertising(&this->advertising_params_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err); | ||||
|     return; | ||||
|     ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err)); | ||||
|     return err; | ||||
|   } | ||||
|  | ||||
|   return ESP_OK; | ||||
| } | ||||
|  | ||||
| void BLEAdvertising::start() { | ||||
|   if (this->current_adv_index_ == -1) { | ||||
|     this->services_advertisement_(); | ||||
|   } else { | ||||
|     this->raw_advertisements_callbacks_[this->current_adv_index_](true); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -124,6 +134,29 @@ void BLEAdvertising::stop() { | ||||
|     ESP_LOGE(TAG, "esp_ble_gap_stop_advertising failed: %d", err); | ||||
|     return; | ||||
|   } | ||||
|   if (this->current_adv_index_ != -1) { | ||||
|     this->raw_advertisements_callbacks_[this->current_adv_index_](false); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLEAdvertising::loop() { | ||||
|   if (this->raw_advertisements_callbacks_.empty()) { | ||||
|     return; | ||||
|   } | ||||
|   const uint32_t now = millis(); | ||||
|   if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { | ||||
|     this->stop(); | ||||
|     this->current_adv_index_ += 1; | ||||
|     if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) { | ||||
|       this->current_adv_index_ = -1; | ||||
|     } | ||||
|     this->start(); | ||||
|     this->last_advertisement_time_ = now; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLEAdvertising::register_raw_advertisement_callback(std::function<void(bool)> &&callback) { | ||||
|   this->raw_advertisements_callbacks_.push_back(std::move(callback)); | ||||
| } | ||||
|  | ||||
| }  // namespace esp32_ble | ||||
|   | ||||
| @@ -1,20 +1,31 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <array> | ||||
| #include <functional> | ||||
| #include <vector> | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_bt.h> | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <esp_gatts_api.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_ble { | ||||
|  | ||||
| using raw_adv_data_t = struct { | ||||
|   uint8_t *data; | ||||
|   size_t length; | ||||
|   esp_power_level_t power_level; | ||||
| }; | ||||
|  | ||||
| class ESPBTUUID; | ||||
|  | ||||
| class BLEAdvertising { | ||||
|  public: | ||||
|   BLEAdvertising(); | ||||
|   BLEAdvertising(uint32_t advertising_cycle_time); | ||||
|  | ||||
|   void loop(); | ||||
|  | ||||
|   void add_service_uuid(ESPBTUUID uuid); | ||||
|   void remove_service_uuid(ESPBTUUID uuid); | ||||
| @@ -22,16 +33,25 @@ class BLEAdvertising { | ||||
|   void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } | ||||
|   void set_manufacturer_data(const std::vector<uint8_t> &data); | ||||
|   void set_service_data(const std::vector<uint8_t> &data); | ||||
|   void register_raw_advertisement_callback(std::function<void(bool)> &&callback); | ||||
|  | ||||
|   void start(); | ||||
|   void stop(); | ||||
|  | ||||
|  protected: | ||||
|   esp_err_t services_advertisement_(); | ||||
|  | ||||
|   bool scan_response_; | ||||
|   esp_ble_adv_data_t advertising_data_; | ||||
|   esp_ble_adv_data_t scan_response_data_; | ||||
|   esp_ble_adv_params_t advertising_params_; | ||||
|   std::vector<ESPBTUUID> advertising_uuids_; | ||||
|  | ||||
|   std::vector<std::function<void(bool)>> raw_advertisements_callbacks_; | ||||
|  | ||||
|   const uint32_t advertising_cycle_time_; | ||||
|   uint32_t last_advertisement_time_{0}; | ||||
|   int8_t current_adv_index_{-1};  // -1 means standard scan response | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_ble | ||||
|   | ||||
| @@ -1,16 +1,21 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components.esp32_ble import CONF_BLE_ID | ||||
| from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER | ||||
| from esphome.core import CORE, TimePeriod | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option | ||||
| from esphome.components import esp32_ble | ||||
|  | ||||
| AUTO_LOAD = ["esp32_ble"] | ||||
| DEPENDENCIES = ["esp32"] | ||||
| CONFLICTS_WITH = ["esp32_ble_tracker"] | ||||
|  | ||||
| esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") | ||||
| ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component) | ||||
|  | ||||
| ESP32BLEBeacon = esp32_ble_beacon_ns.class_( | ||||
|     "ESP32BLEBeacon", | ||||
|     cg.Component, | ||||
|     esp32_ble.GAPEventHandler, | ||||
|     cg.Parented.template(esp32_ble.ESP32BLE), | ||||
| ) | ||||
| CONF_MAJOR = "major" | ||||
| CONF_MINOR = "minor" | ||||
| CONF_MIN_INTERVAL = "min_interval" | ||||
| @@ -28,6 +33,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(ESP32BLEBeacon), | ||||
|             cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), | ||||
|             cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True), | ||||
|             cv.Required(CONF_UUID): cv.uuid, | ||||
|             cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t, | ||||
| @@ -48,7 +54,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 min=-128, max=0 | ||||
|             ), | ||||
|             cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( | ||||
|                 cv.decibel, cv.one_of(-12, -9, -6, -3, 0, 3, 6, 9, int=True) | ||||
|                 cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True) | ||||
|             ), | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
| @@ -62,6 +68,10 @@ async def to_code(config): | ||||
|     uuid = config[CONF_UUID].hex | ||||
|     uuid_arr = [cg.RawExpression(f"0x{uuid[i:i + 2]}") for i in range(0, len(uuid), 2)] | ||||
|     var = cg.new_Pvariable(config[CONF_ID], uuid_arr) | ||||
|  | ||||
|     parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) | ||||
|     cg.add(parent.register_gap_event_handler(var)) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|     cg.add(var.set_major(config[CONF_MAJOR])) | ||||
|     cg.add(var.set_minor(config[CONF_MINOR])) | ||||
|   | ||||
| @@ -3,14 +3,16 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <nvs_flash.h> | ||||
| #include <freertos/FreeRTOS.h> | ||||
| #include <esp_bt_main.h> | ||||
| #include <esp_bt.h> | ||||
| #include <freertos/task.h> | ||||
| #include <esp_bt_main.h> | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <freertos/FreeRTOS.h> | ||||
| #include <freertos/task.h> | ||||
| #include <nvs_flash.h> | ||||
| #include <cstring> | ||||
|  | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
| #include <esp32-hal-bt.h> | ||||
| @@ -21,20 +23,6 @@ namespace esp32_ble_beacon { | ||||
|  | ||||
| static const char *const TAG = "esp32_ble_beacon"; | ||||
|  | ||||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| static esp_ble_adv_params_t ble_adv_params = { | ||||
|     .adv_int_min = 0x20, | ||||
|     .adv_int_max = 0x40, | ||||
|     .adv_type = ADV_TYPE_NONCONN_IND, | ||||
|     .own_addr_type = BLE_ADDR_TYPE_PUBLIC, | ||||
|     .peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | ||||
|     .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, | ||||
|     .channel_map = ADV_CHNL_ALL, | ||||
|     .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, | ||||
| }; | ||||
|  | ||||
| #define ENDIAN_CHANGE_U16(x) ((((x) &0xFF00) >> 8) + (((x) &0xFF) << 8)) | ||||
|  | ||||
| static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = { | ||||
|     .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = {0x4C, 0x00}, .beacon_type = {0x02, 0x15}}; | ||||
|  | ||||
| @@ -53,117 +41,62 @@ void ESP32BLEBeacon::dump_config() { | ||||
|                 "  UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" | ||||
|                 ", TX Power: %ddBm", | ||||
|                 uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_, | ||||
|                 this->tx_power_); | ||||
|                 (this->tx_power_ * 3) - 12); | ||||
| } | ||||
|  | ||||
| float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } | ||||
|  | ||||
| void ESP32BLEBeacon::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ESP32 BLE beacon..."); | ||||
|   global_esp32_ble_beacon = this; | ||||
|   this->ble_adv_params_ = { | ||||
|       .adv_int_min = static_cast<uint16_t>(this->min_interval_ / 0.625f), | ||||
|       .adv_int_max = static_cast<uint16_t>(this->max_interval_ / 0.625f), | ||||
|       .adv_type = ADV_TYPE_NONCONN_IND, | ||||
|       .own_addr_type = BLE_ADDR_TYPE_PUBLIC, | ||||
|       .peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | ||||
|       .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, | ||||
|       .channel_map = ADV_CHNL_ALL, | ||||
|       .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, | ||||
|   }; | ||||
|  | ||||
|   xTaskCreatePinnedToCore(ESP32BLEBeacon::ble_core_task, | ||||
|                           "ble_task",  // name | ||||
|                           10000,       // stack size (in words) | ||||
|                           nullptr,     // input params | ||||
|                           1,           // priority | ||||
|                           nullptr,     // Handle, not needed | ||||
|                           0            // core | ||||
|   ); | ||||
|   global_ble->advertising_register_raw_advertisement_callback([this](bool advertise) { | ||||
|     this->advertising_ = advertise; | ||||
|     if (advertise) { | ||||
|       this->on_advertise_(); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::BLUETOOTH; } | ||||
| void ESP32BLEBeacon::ble_core_task(void *params) { | ||||
|   ble_setup(); | ||||
|  | ||||
|   while (true) { | ||||
|     delay(1000);  // NOLINT | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLEBeacon::ble_setup() { | ||||
|   ble_adv_params.adv_int_min = static_cast<uint16_t>(global_esp32_ble_beacon->min_interval_ / 0.625f); | ||||
|   ble_adv_params.adv_int_max = static_cast<uint16_t>(global_esp32_ble_beacon->max_interval_ / 0.625f); | ||||
|  | ||||
|   // Initialize non-volatile storage for the bluetooth controller | ||||
|   esp_err_t err = nvs_flash_init(); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
|   if (!btStart()) { | ||||
|     ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); | ||||
|     return; | ||||
|   } | ||||
| #else | ||||
|   if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { | ||||
|     // start bt controller | ||||
|     if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { | ||||
|       esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); | ||||
|       err = esp_bt_controller_init(&cfg); | ||||
|       if (err != ESP_OK) { | ||||
|         ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err)); | ||||
|         return; | ||||
|       } | ||||
|       while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) | ||||
|         ; | ||||
|     } | ||||
|     if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) { | ||||
|       err = esp_bt_controller_enable(ESP_BT_MODE_BLE); | ||||
|       if (err != ESP_OK) { | ||||
|         ESP_LOGE(TAG, "esp_bt_controller_enable failed: %s", esp_err_to_name(err)); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { | ||||
|       ESP_LOGE(TAG, "esp bt controller enable failed"); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); | ||||
|  | ||||
|   err = esp_bluedroid_init(); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); | ||||
|     return; | ||||
|   } | ||||
|   err = esp_bluedroid_enable(); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); | ||||
|     return; | ||||
|   } | ||||
|   err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, | ||||
|                              static_cast<esp_power_level_t>((global_esp32_ble_beacon->tx_power_ + 12) / 3)); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); | ||||
|     return; | ||||
|   } | ||||
|   err = esp_ble_gap_register_callback(ESP32BLEBeacon::gap_event_handler); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| void ESP32BLEBeacon::on_advertise_() { | ||||
|   esp_ble_ibeacon_t ibeacon_adv_data; | ||||
|   memcpy(&ibeacon_adv_data.ibeacon_head, &IBEACON_COMMON_HEAD, sizeof(esp_ble_ibeacon_head_t)); | ||||
|   memcpy(&ibeacon_adv_data.ibeacon_vendor.proximity_uuid, global_esp32_ble_beacon->uuid_.data(), | ||||
|   memcpy(&ibeacon_adv_data.ibeacon_vendor.proximity_uuid, this->uuid_.data(), | ||||
|          sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid)); | ||||
|   ibeacon_adv_data.ibeacon_vendor.minor = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->minor_); | ||||
|   ibeacon_adv_data.ibeacon_vendor.major = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->major_); | ||||
|   ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(global_esp32_ble_beacon->measured_power_); | ||||
|   ibeacon_adv_data.ibeacon_vendor.minor = byteswap(this->minor_); | ||||
|   ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_); | ||||
|   ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(this->measured_power_); | ||||
|  | ||||
|   esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); | ||||
|   ESP_LOGD(TAG, "Setting BLE TX power"); | ||||
|   esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); | ||||
|   } | ||||
|   err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err)); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { | ||||
|   if (!this->advertising_) | ||||
|     return; | ||||
|  | ||||
|   esp_err_t err; | ||||
|   switch (event) { | ||||
|     case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: { | ||||
|       err = esp_ble_gap_start_advertising(&ble_adv_params); | ||||
|       err = esp_ble_gap_start_advertising(&this->ble_adv_params_); | ||||
|       if (err != ESP_OK) { | ||||
|         ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err); | ||||
|         ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err)); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
| @@ -181,6 +114,7 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap | ||||
|       } else { | ||||
|         ESP_LOGD(TAG, "BLE stopped advertising successfully"); | ||||
|       } | ||||
|       // this->advertising_ = false; | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
| @@ -188,8 +122,6 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap | ||||
|   } | ||||
| } | ||||
|  | ||||
| ESP32BLEBeacon *global_esp32_ble_beacon = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace esp32_ble_beacon | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -1,39 +1,39 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/esp32_ble/ble.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <esp_bt.h> | ||||
| #include <esp_gap_ble_api.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_ble_beacon { | ||||
|  | ||||
| // NOLINTNEXTLINE(modernize-use-using) | ||||
| typedef struct { | ||||
| using esp_ble_ibeacon_head_t = struct { | ||||
|   uint8_t flags[3]; | ||||
|   uint8_t length; | ||||
|   uint8_t type; | ||||
|   uint8_t company_id[2]; | ||||
|   uint8_t beacon_type[2]; | ||||
| } __attribute__((packed)) esp_ble_ibeacon_head_t; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| // NOLINTNEXTLINE(modernize-use-using) | ||||
| typedef struct { | ||||
| using esp_ble_ibeacon_vendor_t = struct { | ||||
|   uint8_t proximity_uuid[16]; | ||||
|   uint16_t major; | ||||
|   uint16_t minor; | ||||
|   uint8_t measured_power; | ||||
| } __attribute__((packed)) esp_ble_ibeacon_vendor_t; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| // NOLINTNEXTLINE(modernize-use-using) | ||||
| typedef struct { | ||||
| using esp_ble_ibeacon_t = struct { | ||||
|   esp_ble_ibeacon_head_t ibeacon_head; | ||||
|   esp_ble_ibeacon_vendor_t ibeacon_vendor; | ||||
| } __attribute__((packed)) esp_ble_ibeacon_t; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| class ESP32BLEBeacon : public Component { | ||||
| using namespace esp32_ble; | ||||
|  | ||||
| class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented<ESP32BLE> { | ||||
|  public: | ||||
|   explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {} | ||||
|  | ||||
| @@ -46,12 +46,11 @@ class ESP32BLEBeacon : public Component { | ||||
|   void set_min_interval(uint16_t val) { this->min_interval_ = val; } | ||||
|   void set_max_interval(uint16_t val) { this->max_interval_ = val; } | ||||
|   void set_measured_power(int8_t val) { this->measured_power_ = val; } | ||||
|   void set_tx_power(int8_t val) { this->tx_power_ = val; } | ||||
|   void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; } | ||||
|   void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; | ||||
|  | ||||
|  protected: | ||||
|   static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); | ||||
|   static void ble_core_task(void *params); | ||||
|   static void ble_setup(); | ||||
|   void on_advertise_(); | ||||
|  | ||||
|   std::array<uint8_t, 16> uuid_; | ||||
|   uint16_t major_{}; | ||||
| @@ -59,12 +58,11 @@ class ESP32BLEBeacon : public Component { | ||||
|   uint16_t min_interval_{}; | ||||
|   uint16_t max_interval_{}; | ||||
|   int8_t measured_power_{}; | ||||
|   int8_t tx_power_{}; | ||||
|   esp_power_level_t tx_power_{}; | ||||
|   esp_ble_adv_params_t ble_adv_params_; | ||||
|   bool advertising_{false}; | ||||
| }; | ||||
|  | ||||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| extern ESP32BLEBeacon *global_esp32_ble_beacon; | ||||
|  | ||||
| }  // namespace esp32_ble_beacon | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,6 @@ from esphome.components.esp32 import add_idf_sdkconfig_option | ||||
|  | ||||
| AUTO_LOAD = ["esp32_ble"] | ||||
| CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] | ||||
| CONFLICTS_WITH = ["esp32_ble_beacon"] | ||||
| DEPENDENCIES = ["esp32"] | ||||
|  | ||||
| CONF_MANUFACTURER = "manufacturer" | ||||
|   | ||||
| @@ -6,7 +6,6 @@ from esphome.const import CONF_ID | ||||
|  | ||||
| AUTO_LOAD = ["esp32_ble_server"] | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
| CONFLICTS_WITH = ["esp32_ble_beacon"] | ||||
| DEPENDENCIES = ["wifi", "esp32"] | ||||
|  | ||||
| CONF_AUTHORIZED_DURATION = "authorized_duration" | ||||
|   | ||||
| @@ -45,8 +45,8 @@ void FanCall::validate_() { | ||||
|     this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); | ||||
|  | ||||
|   if (this->binary_state_.has_value() && *this->binary_state_) { | ||||
|     // when turning on, if current speed is zero, set speed to 100% | ||||
|     if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0) { | ||||
|     // when turning on, if neither current nor new speed available, set speed to 100% | ||||
|     if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) { | ||||
|       this->speed_ = traits.supported_speed_count(); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ from esphome.const import ( | ||||
|     CONF_PROTOCOL, | ||||
|     CONF_VISUAL, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
|  | ||||
| CODEOWNERS = ["@rob-deutsch"] | ||||
|  | ||||
| @@ -127,3 +128,5 @@ def to_code(config): | ||||
|     cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) | ||||
|  | ||||
|     cg.add_library("tonia/HeatpumpIR", "1.0.27") | ||||
|     if CORE.is_libretiny: | ||||
|         CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import esp32 | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     __version__, | ||||
|     CONF_ESP8266_DISABLE_SSL_SUPPORT, | ||||
|     CONF_ID, | ||||
|     CONF_TIMEOUT, | ||||
|     CONF_METHOD, | ||||
|     CONF_TIMEOUT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_URL, | ||||
|     CONF_ESP8266_DISABLE_SSL_SUPPORT, | ||||
|     __version__, | ||||
| ) | ||||
| from esphome.core import Lambda, CORE | ||||
| from esphome.components import esp32 | ||||
| from esphome.core import CORE, Lambda | ||||
|  | ||||
| DEPENDENCIES = ["network"] | ||||
| AUTO_LOAD = ["json"] | ||||
| @@ -40,6 +40,8 @@ CONF_VERIFY_SSL = "verify_ssl" | ||||
| CONF_FOLLOW_REDIRECTS = "follow_redirects" | ||||
| CONF_REDIRECT_LIMIT = "redirect_limit" | ||||
| CONF_WATCHDOG_TIMEOUT = "watchdog_timeout" | ||||
| CONF_BUFFER_SIZE_RX = "buffer_size_rx" | ||||
| CONF_BUFFER_SIZE_TX = "buffer_size_tx" | ||||
|  | ||||
| CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" | ||||
| CONF_ON_RESPONSE = "on_response" | ||||
| @@ -99,7 +101,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean, | ||||
|             cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_, | ||||
|             cv.Optional( | ||||
|                 CONF_TIMEOUT, default="5s" | ||||
|                 CONF_TIMEOUT, default="4.5s" | ||||
|             ): cv.positive_time_period_milliseconds, | ||||
|             cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All( | ||||
|                 cv.only_on_esp8266, cv.boolean | ||||
| @@ -110,6 +112,12 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 cv.positive_not_null_time_period, | ||||
|                 cv.positive_time_period_milliseconds, | ||||
|             ), | ||||
|             cv.SplitDefault(CONF_BUFFER_SIZE_RX, esp32_idf=512): cv.All( | ||||
|                 cv.uint16_t, cv.only_with_esp_idf | ||||
|             ), | ||||
|             cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32_idf=512): cv.All( | ||||
|                 cv.uint16_t, cv.only_with_esp_idf | ||||
|             ), | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     cv.require_framework_version( | ||||
| @@ -137,6 +145,9 @@ async def to_code(config): | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         if CORE.using_esp_idf: | ||||
|             cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) | ||||
|             cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) | ||||
|  | ||||
|             esp32.add_idf_sdkconfig_option( | ||||
|                 "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", | ||||
|                 config.get(CONF_VERIFY_SSL), | ||||
|   | ||||
| @@ -80,7 +80,7 @@ class HttpRequestComponent : public Component { | ||||
|   const char *useragent_{nullptr}; | ||||
|   bool follow_redirects_; | ||||
|   uint16_t redirect_limit_; | ||||
|   uint16_t timeout_{5000}; | ||||
|   uint16_t timeout_{4500}; | ||||
|   uint32_t watchdog_timeout_{0}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,12 @@ namespace http_request { | ||||
|  | ||||
| static const char *const TAG = "http_request.idf"; | ||||
|  | ||||
| void HttpRequestIDF::dump_config() { | ||||
|   HttpRequestComponent::dump_config(); | ||||
|   ESP_LOGCONFIG(TAG, "  Buffer Size RX: %u", this->buffer_size_rx_); | ||||
|   ESP_LOGCONFIG(TAG, "  Buffer Size TX: %u", this->buffer_size_tx_); | ||||
| } | ||||
|  | ||||
| std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::string method, std::string body, | ||||
|                                                      std::list<Header> headers) { | ||||
|   if (!network::is_connected()) { | ||||
| @@ -52,6 +58,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin | ||||
|   config.timeout_ms = this->timeout_; | ||||
|   config.disable_auto_redirect = !this->follow_redirects_; | ||||
|   config.max_redirection_count = this->redirect_limit_; | ||||
|   config.auth_type = HTTP_AUTH_TYPE_BASIC; | ||||
| #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE | ||||
|   if (secure) { | ||||
|     config.crt_bundle_attach = esp_crt_bundle_attach; | ||||
| @@ -62,6 +69,9 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin | ||||
|     config.user_agent = this->useragent_; | ||||
|   } | ||||
|  | ||||
|   config.buffer_size = this->buffer_size_rx_; | ||||
|   config.buffer_size_tx = this->buffer_size_tx_; | ||||
|  | ||||
|   const uint32_t start = millis(); | ||||
|   watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); | ||||
|  | ||||
| @@ -76,7 +86,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin | ||||
|     esp_http_client_set_header(client, header.name, header.value); | ||||
|   } | ||||
|  | ||||
|   int body_len = body.length(); | ||||
|   const int body_len = body.length(); | ||||
|  | ||||
|   esp_err_t err = esp_http_client_open(client, body_len); | ||||
|   if (err != ESP_OK) { | ||||
| @@ -108,18 +118,62 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin | ||||
|     return nullptr; | ||||
|   } | ||||
|  | ||||
|   container->content_length = esp_http_client_fetch_headers(client); | ||||
|   const auto status_code = esp_http_client_get_status_code(client); | ||||
|   container->status_code = status_code; | ||||
|   auto is_ok = [](int code) { return code >= HttpStatus_Ok && code < HttpStatus_MultipleChoices; }; | ||||
|  | ||||
|   if (status_code < 200 || status_code >= 300) { | ||||
|     ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), status_code); | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     esp_http_client_cleanup(client); | ||||
|     return nullptr; | ||||
|   container->content_length = esp_http_client_fetch_headers(client); | ||||
|   container->status_code = esp_http_client_get_status_code(client); | ||||
|   if (is_ok(container->status_code)) { | ||||
|     container->duration_ms = millis() - start; | ||||
|     return container; | ||||
|   } | ||||
|   container->duration_ms = millis() - start; | ||||
|   return container; | ||||
|  | ||||
|   if (this->follow_redirects_) { | ||||
|     auto is_redirect = [](int code) { | ||||
|       return code == HttpStatus_MovedPermanently || code == HttpStatus_Found || code == HttpStatus_SeeOther || | ||||
|              code == HttpStatus_TemporaryRedirect || code == HttpStatus_PermanentRedirect; | ||||
|     }; | ||||
|     auto num_redirects = this->redirect_limit_; | ||||
|     while (is_redirect(container->status_code) && num_redirects > 0) { | ||||
|       err = esp_http_client_set_redirection(client); | ||||
|       if (err != ESP_OK) { | ||||
|         ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err)); | ||||
|         this->status_momentary_error("failed", 1000); | ||||
|         esp_http_client_cleanup(client); | ||||
|         return nullptr; | ||||
|       } | ||||
| #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE | ||||
|       char url[256]{}; | ||||
|       if (esp_http_client_get_url(client, url, sizeof(url) - 1) == ESP_OK) { | ||||
|         ESP_LOGV(TAG, "redirecting to url: %s", url); | ||||
|       } | ||||
| #endif | ||||
|       err = esp_http_client_open(client, 0); | ||||
|       if (err != ESP_OK) { | ||||
|         ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err)); | ||||
|         this->status_momentary_error("failed", 1000); | ||||
|         esp_http_client_cleanup(client); | ||||
|         return nullptr; | ||||
|       } | ||||
|  | ||||
|       container->content_length = esp_http_client_fetch_headers(client); | ||||
|       container->status_code = esp_http_client_get_status_code(client); | ||||
|       if (is_ok(container->status_code)) { | ||||
|         container->duration_ms = millis() - start; | ||||
|         return container; | ||||
|       } | ||||
|  | ||||
|       num_redirects--; | ||||
|     } | ||||
|  | ||||
|     if (num_redirects == 0) { | ||||
|       ESP_LOGW(TAG, "Reach redirect limit count=%d", this->redirect_limit_); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); | ||||
|   this->status_momentary_error("failed", 1000); | ||||
|   esp_http_client_cleanup(client); | ||||
|   return nullptr; | ||||
| } | ||||
|  | ||||
| int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { | ||||
|   | ||||
| @@ -24,8 +24,18 @@ class HttpContainerIDF : public HttpContainer { | ||||
|  | ||||
| class HttpRequestIDF : public HttpRequestComponent { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|  | ||||
|   std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body, | ||||
|                                        std::list<Header> headers) override; | ||||
|  | ||||
|   void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; } | ||||
|   void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; } | ||||
|  | ||||
|  protected: | ||||
|   // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE | ||||
|   uint16_t buffer_size_rx_{}; | ||||
|   uint16_t buffer_size_tx_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace http_request | ||||
|   | ||||
| @@ -25,6 +25,10 @@ CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" | ||||
| CONF_I2S_AUDIO = "i2s_audio" | ||||
| CONF_I2S_AUDIO_ID = "i2s_audio_id" | ||||
|  | ||||
| CONF_I2S_MODE = "i2s_mode" | ||||
| CONF_PRIMARY = "primary" | ||||
| CONF_SECONDARY = "secondary" | ||||
|  | ||||
| i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") | ||||
| I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) | ||||
| I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", cg.Parented.template(I2SAudioComponent)) | ||||
| @@ -32,6 +36,12 @@ I2SAudioOut = i2s_audio_ns.class_( | ||||
|     "I2SAudioOut", cg.Parented.template(I2SAudioComponent) | ||||
| ) | ||||
|  | ||||
| i2s_mode_t = cg.global_ns.enum("i2s_mode_t") | ||||
| I2S_MODE_OPTIONS = { | ||||
|     CONF_PRIMARY: i2s_mode_t.I2S_MODE_MASTER,  # NOLINT | ||||
|     CONF_SECONDARY: i2s_mode_t.I2S_MODE_SLAVE,  # NOLINT | ||||
| } | ||||
|  | ||||
| # https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h | ||||
| I2S_PORTS = { | ||||
|     VARIANT_ESP32: 2, | ||||
|   | ||||
| @@ -7,6 +7,9 @@ from esphome.components import microphone, esp32 | ||||
| from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin | ||||
|  | ||||
| from .. import ( | ||||
|     CONF_I2S_MODE, | ||||
|     CONF_PRIMARY, | ||||
|     I2S_MODE_OPTIONS, | ||||
|     i2s_audio_ns, | ||||
|     I2SAudioComponent, | ||||
|     I2SAudioIn, | ||||
| @@ -68,6 +71,9 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( | ||||
|             _validate_bits, cv.enum(BITS_PER_SAMPLE) | ||||
|         ), | ||||
|         cv.Optional(CONF_USE_APLL, default=False): cv.boolean, | ||||
|         cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum( | ||||
|             I2S_MODE_OPTIONS, lower=True | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
| @@ -107,6 +113,7 @@ async def to_code(config): | ||||
|         cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) | ||||
|         cg.add(var.set_pdm(config[CONF_PDM])) | ||||
|  | ||||
|     cg.add(var.set_i2s_mode(config[CONF_I2S_MODE])) | ||||
|     cg.add(var.set_channel(config[CONF_CHANNEL])) | ||||
|     cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) | ||||
|     cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) | ||||
|   | ||||
| @@ -46,7 +46,7 @@ void I2SAudioMicrophone::start_() { | ||||
|     return;  // Waiting for another i2s to return lock | ||||
|   } | ||||
|   i2s_driver_config_t config = { | ||||
|       .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), | ||||
|       .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX), | ||||
|       .sample_rate = this->sample_rate_, | ||||
|       .bits_per_sample = this->bits_per_sample_, | ||||
|       .channel_format = this->channel_, | ||||
|   | ||||
| @@ -30,6 +30,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; } | ||||
|  | ||||
|   void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } | ||||
|   void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } | ||||
|   void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } | ||||
| @@ -46,6 +48,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | ||||
|   bool adc_{false}; | ||||
| #endif | ||||
|   bool pdm_{false}; | ||||
|   i2s_mode_t i2s_mode_{}; | ||||
|   i2s_channel_fmt_t channel_; | ||||
|   uint32_t sample_rate_; | ||||
|   i2s_bits_per_sample_t bits_per_sample_; | ||||
|   | ||||
| @@ -92,7 +92,9 @@ static const uint8_t ILI9XXX_GMCTRN1 = 0xE1; | ||||
|  | ||||
| static const uint8_t ILI9XXX_CSCON = 0xF0; | ||||
| static const uint8_t ILI9XXX_ADJCTL3 = 0xF7; | ||||
| static const uint8_t ILI9XXX_DELAY = 0xFF;  // followed by one byte of delay time in ms | ||||
| static const uint8_t ILI9XXX_DELAY_FLAG = 0xFF; | ||||
| // special marker for delay - command byte reprents ms, length byte is an impossible value | ||||
| #define ILI9XXX_DELAY(ms) ((uint8_t) ((ms) | 0x80)), ILI9XXX_DELAY_FLAG | ||||
|  | ||||
| }  // namespace ili9xxx | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -34,8 +34,8 @@ void ILI9XXXDisplay::setup() { | ||||
|   ESP_LOGD(TAG, "Setting up ILI9xxx"); | ||||
|  | ||||
|   this->setup_pins_(); | ||||
|   this->init_lcd(this->init_sequence_); | ||||
|   this->init_lcd(this->extra_init_sequence_.data()); | ||||
|   this->init_lcd_(this->init_sequence_); | ||||
|   this->init_lcd_(this->extra_init_sequence_.data()); | ||||
|   switch (this->pixel_mode_) { | ||||
|     case PIXEL_MODE_16: | ||||
|       if (this->is_18bitdisplay_) { | ||||
| @@ -405,42 +405,29 @@ void ILI9XXXDisplay::reset_() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ILI9XXXDisplay::init_lcd(const uint8_t *addr) { | ||||
| void ILI9XXXDisplay::init_lcd_(const uint8_t *addr) { | ||||
|   if (addr == nullptr) | ||||
|     return; | ||||
|   uint8_t cmd, x, num_args; | ||||
|   while ((cmd = *addr++) != 0) { | ||||
|     x = *addr++; | ||||
|     if (cmd == ILI9XXX_DELAY) { | ||||
|       ESP_LOGD(TAG, "Delay %dms", x); | ||||
|       delay(x); | ||||
|     if (x == ILI9XXX_DELAY_FLAG) { | ||||
|       cmd &= 0x7F; | ||||
|       ESP_LOGV(TAG, "Delay %dms", cmd); | ||||
|       delay(cmd); | ||||
|     } else { | ||||
|       num_args = x & 0x7F; | ||||
|       ESP_LOGD(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, *addr); | ||||
|       ESP_LOGV(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, *addr); | ||||
|       this->send_command(cmd, addr, num_args); | ||||
|       addr += num_args; | ||||
|       if (x & 0x80) { | ||||
|         ESP_LOGD(TAG, "Delay 150ms"); | ||||
|         ESP_LOGV(TAG, "Delay 150ms"); | ||||
|         delay(150);  // NOLINT | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ILI9XXXGC9A01A::init_lcd(const uint8_t *addr) { | ||||
|   if (addr == nullptr) | ||||
|     return; | ||||
|   uint8_t cmd, x, num_args; | ||||
|   while ((cmd = *addr++) != 0) { | ||||
|     x = *addr++; | ||||
|     num_args = x & 0x7F; | ||||
|     this->send_command(cmd, addr, num_args); | ||||
|     addr += num_args; | ||||
|     if (x & 0x80) | ||||
|       delay(150);  // NOLINT | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Tell the display controller where we want to draw pixels. | ||||
| void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { | ||||
|   x1 += this->offset_x_; | ||||
|   | ||||
| @@ -33,7 +33,9 @@ class ILI9XXXDisplay : public display::DisplayBuffer, | ||||
|     uint8_t cmd, num_args, bits; | ||||
|     const uint8_t *addr = init_sequence; | ||||
|     while ((cmd = *addr++) != 0) { | ||||
|       num_args = *addr++ & 0x7F; | ||||
|       num_args = *addr++; | ||||
|       if (num_args == ILI9XXX_DELAY_FLAG) | ||||
|         continue; | ||||
|       bits = *addr; | ||||
|       switch (cmd) { | ||||
|         case ILI9XXX_MADCTL: { | ||||
| @@ -50,13 +52,10 @@ class ILI9XXXDisplay : public display::DisplayBuffer, | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         case ILI9XXX_DELAY: | ||||
|           continue;  // no args to skip | ||||
|  | ||||
|         default: | ||||
|           break; | ||||
|       } | ||||
|       addr += num_args; | ||||
|       addr += (num_args & 0x7F); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -109,7 +108,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer, | ||||
|  | ||||
|   virtual void set_madctl(); | ||||
|   void display_(); | ||||
|   virtual void init_lcd(const uint8_t *addr); | ||||
|   void init_lcd_(const uint8_t *addr); | ||||
|   void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2); | ||||
|   void reset_(); | ||||
|  | ||||
| @@ -269,7 +268,6 @@ class ILI9XXXS3BoxLite : public ILI9XXXDisplay { | ||||
| class ILI9XXXGC9A01A : public ILI9XXXDisplay { | ||||
|  public: | ||||
|   ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {} | ||||
|   void init_lcd(const uint8_t *addr) override; | ||||
| }; | ||||
|  | ||||
| //-----------   ILI9XXX_24_TFT display -------------- | ||||
|   | ||||
| @@ -372,9 +372,9 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = { | ||||
|  | ||||
| static const uint8_t PROGMEM INITCMD_ST7735[] = { | ||||
|     ILI9XXX_SWRESET, 0,         // Soft reset, then delay 10ms | ||||
|     ILI9XXX_DELAY, 10, | ||||
|     ILI9XXX_DELAY(10), | ||||
|     ILI9XXX_SLPOUT  , 0,                // Exit Sleep, delay | ||||
|     ILI9XXX_DELAY, 10, | ||||
|     ILI9XXX_DELAY(10), | ||||
|     ILI9XXX_PIXFMT  , 1, 0x05, | ||||
|     ILI9XXX_FRMCTR1, 3, //  4: Frame rate control, 3 args + delay: | ||||
|     0x01, 0x2C, 0x2D,             //     Rate = fosc/(1x2+40) * (LINE+2C+2D) | ||||
| @@ -415,9 +415,9 @@ static const uint8_t PROGMEM INITCMD_ST7735[] = { | ||||
|     0x00, 0x00, 0x02, 0x10, | ||||
|     ILI9XXX_MADCTL  , 1, 0x00,             // Memory Access Control, BGR | ||||
|     ILI9XXX_NORON  , 0, | ||||
|     ILI9XXX_DELAY, 10, | ||||
|     ILI9XXX_DELAY(10), | ||||
|     ILI9XXX_DISPON  , 0,                // Display on | ||||
|     ILI9XXX_DELAY, 10, | ||||
|     ILI9XXX_DELAY(10), | ||||
|     00,   // endo of list | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( | ||||
| CONFIG_SCHEMA = cv.invalid( | ||||
|     "The kalman_combinator sensor has moved.\nPlease use the combination platform instead with type: kalman.\n" | ||||
|     "See https://esphome.io/components/sensor/combination.html" | ||||
| ) | ||||
|   | ||||
							
								
								
									
										212
									
								
								esphome/components/lvgl/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								esphome/components/lvgl/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | ||||
| import logging | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.display import Display | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_AUTO_CLEAR_ENABLED, | ||||
|     CONF_BUFFER_SIZE, | ||||
|     CONF_ID, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_PAGES, | ||||
| ) | ||||
| from esphome.core import CORE, ID, Lambda | ||||
| from esphome.cpp_generator import MockObj | ||||
| from esphome.final_validate import full_config | ||||
| from esphome.helpers import write_file_if_changed | ||||
|  | ||||
| from . import defines as df, helpers, lv_validation as lvalid | ||||
| from .label import label_spec | ||||
| from .lvcode import ConstantLiteral, LvContext | ||||
|  | ||||
| # from .menu import menu_spec | ||||
| from .obj import obj_spec | ||||
| from .schemas import WIDGET_TYPES, any_widget_schema, obj_schema | ||||
| from .types import FontEngine, LvglComponent, lv_disp_t_ptr, lv_font_t, lvgl_ns | ||||
| from .widget import LvScrActType, Widget, add_widgets, set_obj_properties | ||||
|  | ||||
| DOMAIN = "lvgl" | ||||
| DEPENDENCIES = ("display",) | ||||
| AUTO_LOAD = ("key_provider",) | ||||
| CODEOWNERS = ("@clydebarrow",) | ||||
| LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| for widg in ( | ||||
|     label_spec, | ||||
|     obj_spec, | ||||
| ): | ||||
|     WIDGET_TYPES[widg.name] = widg | ||||
|  | ||||
| lv_scr_act_spec = LvScrActType() | ||||
| lv_scr_act = Widget.create( | ||||
|     None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None | ||||
| ) | ||||
|  | ||||
| WIDGET_SCHEMA = any_widget_schema() | ||||
|  | ||||
|  | ||||
| async def add_init_lambda(lv_component, init): | ||||
|     if init: | ||||
|         lamb = await cg.process_lambda(Lambda(init), [(lv_disp_t_ptr, "lv_disp")]) | ||||
|         cg.add(lv_component.add_init_lambda(lamb)) | ||||
|  | ||||
|  | ||||
| lv_defines = {}  # Dict of #defines to provide as build flags | ||||
|  | ||||
|  | ||||
| def add_define(macro, value="1"): | ||||
|     if macro in lv_defines and lv_defines[macro] != value: | ||||
|         LOGGER.error( | ||||
|             "Redefinition of %s - was %s now %s", macro, lv_defines[macro], value | ||||
|         ) | ||||
|     lv_defines[macro] = value | ||||
|  | ||||
|  | ||||
| def as_macro(macro, value): | ||||
|     if value is None: | ||||
|         return f"#define {macro}" | ||||
|     return f"#define {macro} {value}" | ||||
|  | ||||
|  | ||||
| LV_CONF_FILENAME = "lv_conf.h" | ||||
| LV_CONF_H_FORMAT = """\ | ||||
| #pragma once | ||||
| {} | ||||
| """ | ||||
|  | ||||
|  | ||||
| def generate_lv_conf_h(): | ||||
|     definitions = [as_macro(m, v) for m, v in lv_defines.items()] | ||||
|     definitions.sort() | ||||
|     return LV_CONF_H_FORMAT.format("\n".join(definitions)) | ||||
|  | ||||
|  | ||||
| def final_validation(config): | ||||
|     global_config = full_config.get() | ||||
|     for display_id in config[df.CONF_DISPLAYS]: | ||||
|         path = global_config.get_path_for_id(display_id)[:-1] | ||||
|         display = global_config.get_config_for_path(path) | ||||
|         if CONF_LAMBDA in display: | ||||
|             raise cv.Invalid("Using lambda: in display config not compatible with LVGL") | ||||
|         if display[CONF_AUTO_CLEAR_ENABLED]: | ||||
|             raise cv.Invalid( | ||||
|                 "Using auto_clear_enabled: true in display config not compatible with LVGL" | ||||
|             ) | ||||
|     buffer_frac = config[CONF_BUFFER_SIZE] | ||||
|     if not CORE.is_host and buffer_frac > 0.5 and "psram" not in global_config: | ||||
|         LOGGER.warning("buffer_size: may need to be reduced without PSRAM") | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     cg.add_library("lvgl/lvgl", "8.4.0") | ||||
|     CORE.add_define("USE_LVGL") | ||||
|     # suppress default enabling of extra widgets | ||||
|     add_define("_LV_KCONFIG_PRESENT") | ||||
|     # Always enable - lots of things use it. | ||||
|     add_define("LV_DRAW_COMPLEX", "1") | ||||
|     add_define("LV_TICK_CUSTOM", "1") | ||||
|     add_define("LV_TICK_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') | ||||
|     add_define("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(lv_millis())") | ||||
|     add_define("LV_MEM_CUSTOM", "1") | ||||
|     add_define("LV_MEM_CUSTOM_ALLOC", "lv_custom_mem_alloc") | ||||
|     add_define("LV_MEM_CUSTOM_FREE", "lv_custom_mem_free") | ||||
|     add_define("LV_MEM_CUSTOM_REALLOC", "lv_custom_mem_realloc") | ||||
|     add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') | ||||
|  | ||||
|     add_define("LV_LOG_LEVEL", f"LV_LOG_LEVEL_{config[df.CONF_LOG_LEVEL]}") | ||||
|     add_define("LV_COLOR_DEPTH", config[df.CONF_COLOR_DEPTH]) | ||||
|     for font in helpers.lv_fonts_used: | ||||
|         add_define(f"LV_FONT_{font.upper()}") | ||||
|  | ||||
|     if config[df.CONF_COLOR_DEPTH] == 16: | ||||
|         add_define( | ||||
|             "LV_COLOR_16_SWAP", | ||||
|             "1" if config[df.CONF_BYTE_ORDER] == "big_endian" else "0", | ||||
|         ) | ||||
|     add_define( | ||||
|         "LV_COLOR_CHROMA_KEY", | ||||
|         await lvalid.lv_color.process(config[df.CONF_TRANSPARENCY_KEY]), | ||||
|     ) | ||||
|     CORE.add_build_flag("-Isrc") | ||||
|  | ||||
|     cg.add_global(lvgl_ns.using) | ||||
|     lv_component = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(lv_component, config) | ||||
|     Widget.create(config[CONF_ID], lv_component, WIDGET_TYPES[df.CONF_OBJ], config) | ||||
|     for display in config[df.CONF_DISPLAYS]: | ||||
|         cg.add(lv_component.add_display(await cg.get_variable(display))) | ||||
|  | ||||
|     frac = config[CONF_BUFFER_SIZE] | ||||
|     if frac >= 0.75: | ||||
|         frac = 1 | ||||
|     elif frac >= 0.375: | ||||
|         frac = 2 | ||||
|     elif frac > 0.19: | ||||
|         frac = 4 | ||||
|     else: | ||||
|         frac = 8 | ||||
|     cg.add(lv_component.set_buffer_frac(int(frac))) | ||||
|     cg.add(lv_component.set_full_refresh(config[df.CONF_FULL_REFRESH])) | ||||
|  | ||||
|     for font in helpers.esphome_fonts_used: | ||||
|         await cg.get_variable(font) | ||||
|         cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) | ||||
|     default_font = config[df.CONF_DEFAULT_FONT] | ||||
|     if default_font not in helpers.lv_fonts_used: | ||||
|         add_define( | ||||
|             "LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})" | ||||
|         ) | ||||
|         globfont_id = ID( | ||||
|             df.DEFAULT_ESPHOME_FONT, | ||||
|             True, | ||||
|             type=lv_font_t.operator("ptr").operator("const"), | ||||
|         ) | ||||
|         cg.new_variable(globfont_id, MockObj(default_font)) | ||||
|         add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) | ||||
|     else: | ||||
|         add_define("LV_FONT_DEFAULT", default_font) | ||||
|  | ||||
|     with LvContext(): | ||||
|         await set_obj_properties(lv_scr_act, config) | ||||
|         await add_widgets(lv_scr_act, config) | ||||
|     Widget.set_completed() | ||||
|     await add_init_lambda(lv_component, LvContext.get_code()) | ||||
|     for comp in helpers.lvgl_components_required: | ||||
|         CORE.add_define(f"USE_LVGL_{comp.upper()}") | ||||
|     for use in helpers.lv_uses: | ||||
|         add_define(f"LV_USE_{use.upper()}") | ||||
|     lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) | ||||
|     write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) | ||||
|     CORE.add_build_flag("-DLV_CONF_H=1") | ||||
|     CORE.add_build_flag(f'-DLV_CONF_PATH="{LV_CONF_FILENAME}"') | ||||
|  | ||||
|  | ||||
| def display_schema(config): | ||||
|     value = cv.ensure_list(cv.use_id(Display))(config) | ||||
|     return value or [cv.use_id(Display)(config)] | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = final_validation | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.polling_component_schema("1s") | ||||
|     .extend(obj_schema("obj")) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), | ||||
|             cv.GenerateID(df.CONF_DISPLAYS): display_schema, | ||||
|             cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16), | ||||
|             cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font, | ||||
|             cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage, | ||||
|             cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of( | ||||
|                 *df.LOG_LEVELS, upper=True | ||||
|             ), | ||||
|             cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( | ||||
|                 "big_endian", "little_endian" | ||||
|             ), | ||||
|             cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA), | ||||
|             cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, | ||||
|         } | ||||
|     ) | ||||
| ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) | ||||
							
								
								
									
										487
									
								
								esphome/components/lvgl/defines.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										487
									
								
								esphome/components/lvgl/defines.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,487 @@ | ||||
| """ | ||||
| This is the base of the import tree for LVGL. It contains constant definitions used elsewhere. | ||||
| Constants already defined in esphome.const are not duplicated here and must be imported where used. | ||||
|  | ||||
| """ | ||||
|  | ||||
| from esphome import codegen as cg, config_validation as cv | ||||
| from esphome.core import ID, Lambda | ||||
| from esphome.cpp_types import uint32 | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
|  | ||||
| from .lvcode import ConstantLiteral | ||||
|  | ||||
|  | ||||
| class LValidator: | ||||
|     """ | ||||
|     A validator for a particular type used in LVGL. Usable in configs as a validator, also | ||||
|     has `process()` to convert a value during code generation | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, validator, rtype, idtype=None, idexpr=None, retmapper=None): | ||||
|         self.validator = validator | ||||
|         self.rtype = rtype | ||||
|         self.idtype = idtype | ||||
|         self.idexpr = idexpr | ||||
|         self.retmapper = retmapper | ||||
|  | ||||
|     def __call__(self, value): | ||||
|         if isinstance(value, cv.Lambda): | ||||
|             return cv.returning_lambda(value) | ||||
|         if self.idtype is not None and isinstance(value, ID): | ||||
|             return cv.use_id(self.idtype)(value) | ||||
|         return self.validator(value) | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|         if value is None: | ||||
|             return None | ||||
|         if isinstance(value, Lambda): | ||||
|             return cg.RawExpression( | ||||
|                 f"{await cg.process_lambda(value, args, return_type=self.rtype)}()" | ||||
|             ) | ||||
|         if self.idtype is not None and isinstance(value, ID): | ||||
|             return cg.RawExpression(f"{value}->{self.idexpr}") | ||||
|         if self.retmapper is not None: | ||||
|             return self.retmapper(value) | ||||
|         return cg.safe_exp(value) | ||||
|  | ||||
|  | ||||
| class LvConstant(LValidator): | ||||
|     """ | ||||
|     Allow one of a list of choices, mapped to upper case, and prepend the choice with the prefix. | ||||
|     It's also permitted to include the prefix in the value | ||||
|     The property `one_of` has the single case validator, and `several_of` allows a list of constants. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, prefix: str, *choices): | ||||
|         self.prefix = prefix | ||||
|         self.choices = choices | ||||
|         prefixed_choices = [prefix + v for v in choices] | ||||
|         prefixed_validator = cv.one_of(*prefixed_choices, upper=True) | ||||
|  | ||||
|         @schema_extractor("one_of") | ||||
|         def validator(value): | ||||
|             if value == SCHEMA_EXTRACT: | ||||
|                 return self.choices | ||||
|             if isinstance(value, str) and value.startswith(self.prefix): | ||||
|                 return prefixed_validator(value) | ||||
|             return self.prefix + cv.one_of(*choices, upper=True)(value) | ||||
|  | ||||
|         super().__init__(validator, rtype=uint32) | ||||
|         self.one_of = LValidator(validator, uint32, retmapper=self.mapper) | ||||
|         self.several_of = LValidator( | ||||
|             cv.ensure_list(self.one_of), uint32, retmapper=self.mapper | ||||
|         ) | ||||
|  | ||||
|     def mapper(self, value, args=()): | ||||
|         if isinstance(value, list): | ||||
|             value = "|".join(value) | ||||
|         return ConstantLiteral(value) | ||||
|  | ||||
|     def extend(self, *choices): | ||||
|         """ | ||||
|         Extend an LVCconstant with additional choices. | ||||
|         :param choices: The extra choices | ||||
|         :return: A new LVConstant instance | ||||
|         """ | ||||
|         return LvConstant(self.prefix, *(self.choices + choices)) | ||||
|  | ||||
|  | ||||
| # Widgets | ||||
| CONF_LABEL = "label" | ||||
|  | ||||
| # Parts | ||||
| CONF_MAIN = "main" | ||||
| CONF_SCROLLBAR = "scrollbar" | ||||
| CONF_INDICATOR = "indicator" | ||||
| CONF_KNOB = "knob" | ||||
| CONF_SELECTED = "selected" | ||||
| CONF_ITEMS = "items" | ||||
| CONF_TICKS = "ticks" | ||||
| CONF_TICK_STYLE = "tick_style" | ||||
| CONF_CURSOR = "cursor" | ||||
| CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder" | ||||
|  | ||||
| LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ | ||||
|     "dejavu_16_persian_hebrew", | ||||
|     "simsun_16_cjk", | ||||
|     "unscii_8", | ||||
|     "unscii_16", | ||||
| ] | ||||
|  | ||||
| LV_EVENT = { | ||||
|     "PRESS": "PRESSED", | ||||
|     "SHORT_CLICK": "SHORT_CLICKED", | ||||
|     "LONG_PRESS": "LONG_PRESSED", | ||||
|     "LONG_PRESS_REPEAT": "LONG_PRESSED_REPEAT", | ||||
|     "CLICK": "CLICKED", | ||||
|     "RELEASE": "RELEASED", | ||||
|     "SCROLL_BEGIN": "SCROLL_BEGIN", | ||||
|     "SCROLL_END": "SCROLL_END", | ||||
|     "SCROLL": "SCROLL", | ||||
|     "FOCUS": "FOCUSED", | ||||
|     "DEFOCUS": "DEFOCUSED", | ||||
|     "READY": "READY", | ||||
|     "CANCEL": "CANCEL", | ||||
| } | ||||
|  | ||||
| LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT) | ||||
|  | ||||
|  | ||||
| LV_ANIM = LvConstant( | ||||
|     "LV_SCR_LOAD_ANIM_", | ||||
|     "NONE", | ||||
|     "OVER_LEFT", | ||||
|     "OVER_RIGHT", | ||||
|     "OVER_TOP", | ||||
|     "OVER_BOTTOM", | ||||
|     "MOVE_LEFT", | ||||
|     "MOVE_RIGHT", | ||||
|     "MOVE_TOP", | ||||
|     "MOVE_BOTTOM", | ||||
|     "FADE_IN", | ||||
|     "FADE_OUT", | ||||
|     "OUT_LEFT", | ||||
|     "OUT_RIGHT", | ||||
|     "OUT_TOP", | ||||
|     "OUT_BOTTOM", | ||||
| ) | ||||
|  | ||||
| LOG_LEVELS = ( | ||||
|     "TRACE", | ||||
|     "INFO", | ||||
|     "WARN", | ||||
|     "ERROR", | ||||
|     "USER", | ||||
|     "NONE", | ||||
| ) | ||||
|  | ||||
| LV_LONG_MODES = LvConstant( | ||||
|     "LV_LABEL_LONG_", | ||||
|     "WRAP", | ||||
|     "DOT", | ||||
|     "SCROLL", | ||||
|     "SCROLL_CIRCULAR", | ||||
|     "CLIP", | ||||
| ) | ||||
|  | ||||
| STATES = ( | ||||
|     "default", | ||||
|     "checked", | ||||
|     "focused", | ||||
|     "focus_key", | ||||
|     "edited", | ||||
|     "hovered", | ||||
|     "pressed", | ||||
|     "scrolled", | ||||
|     "disabled", | ||||
|     "user_1", | ||||
|     "user_2", | ||||
|     "user_3", | ||||
|     "user_4", | ||||
| ) | ||||
|  | ||||
| PARTS = ( | ||||
|     CONF_MAIN, | ||||
|     CONF_SCROLLBAR, | ||||
|     CONF_INDICATOR, | ||||
|     CONF_KNOB, | ||||
|     CONF_SELECTED, | ||||
|     CONF_ITEMS, | ||||
|     CONF_TICKS, | ||||
|     CONF_CURSOR, | ||||
|     CONF_TEXTAREA_PLACEHOLDER, | ||||
| ) | ||||
|  | ||||
| KEYBOARD_MODES = LvConstant( | ||||
|     "LV_KEYBOARD_MODE_", | ||||
|     "TEXT_LOWER", | ||||
|     "TEXT_UPPER", | ||||
|     "SPECIAL", | ||||
|     "NUMBER", | ||||
| ) | ||||
| ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE") | ||||
| DIRECTIONS = LvConstant("LV_DIR_", "LEFT", "RIGHT", "BOTTOM", "TOP") | ||||
| TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL") | ||||
| CHILD_ALIGNMENTS = LvConstant( | ||||
|     "LV_ALIGN_", | ||||
|     "TOP_LEFT", | ||||
|     "TOP_MID", | ||||
|     "TOP_RIGHT", | ||||
|     "LEFT_MID", | ||||
|     "CENTER", | ||||
|     "RIGHT_MID", | ||||
|     "BOTTOM_LEFT", | ||||
|     "BOTTOM_MID", | ||||
|     "BOTTOM_RIGHT", | ||||
| ) | ||||
|  | ||||
| SIBLING_ALIGNMENTS = LvConstant( | ||||
|     "LV_ALIGN_", | ||||
|     "OUT_LEFT_TOP", | ||||
|     "OUT_TOP_LEFT", | ||||
|     "OUT_TOP_MID", | ||||
|     "OUT_TOP_RIGHT", | ||||
|     "OUT_RIGHT_TOP", | ||||
|     "OUT_LEFT_MID", | ||||
|     "OUT_RIGHT_MID", | ||||
|     "OUT_LEFT_BOTTOM", | ||||
|     "OUT_BOTTOM_LEFT", | ||||
|     "OUT_BOTTOM_MID", | ||||
|     "OUT_BOTTOM_RIGHT", | ||||
|     "OUT_RIGHT_BOTTOM", | ||||
| ) | ||||
| ALIGN_ALIGNMENTS = CHILD_ALIGNMENTS.extend(*SIBLING_ALIGNMENTS.choices) | ||||
|  | ||||
| FLEX_FLOWS = LvConstant( | ||||
|     "LV_FLEX_FLOW_", | ||||
|     "ROW", | ||||
|     "COLUMN", | ||||
|     "ROW_WRAP", | ||||
|     "COLUMN_WRAP", | ||||
|     "ROW_REVERSE", | ||||
|     "COLUMN_REVERSE", | ||||
|     "ROW_WRAP_REVERSE", | ||||
|     "COLUMN_WRAP_REVERSE", | ||||
| ) | ||||
|  | ||||
| OBJ_FLAGS = ( | ||||
|     "hidden", | ||||
|     "clickable", | ||||
|     "click_focusable", | ||||
|     "checkable", | ||||
|     "scrollable", | ||||
|     "scroll_elastic", | ||||
|     "scroll_momentum", | ||||
|     "scroll_one", | ||||
|     "scroll_chain_hor", | ||||
|     "scroll_chain_ver", | ||||
|     "scroll_chain", | ||||
|     "scroll_on_focus", | ||||
|     "scroll_with_arrow", | ||||
|     "snappable", | ||||
|     "press_lock", | ||||
|     "event_bubble", | ||||
|     "gesture_bubble", | ||||
|     "adv_hittest", | ||||
|     "ignore_layout", | ||||
|     "floating", | ||||
|     "overflow_visible", | ||||
|     "layout_1", | ||||
|     "layout_2", | ||||
|     "widget_1", | ||||
|     "widget_2", | ||||
|     "user_1", | ||||
|     "user_2", | ||||
|     "user_3", | ||||
|     "user_4", | ||||
| ) | ||||
|  | ||||
| ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") | ||||
| BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") | ||||
|  | ||||
| BTNMATRIX_CTRLS = ( | ||||
|     "HIDDEN", | ||||
|     "NO_REPEAT", | ||||
|     "DISABLED", | ||||
|     "CHECKABLE", | ||||
|     "CHECKED", | ||||
|     "CLICK_TRIG", | ||||
|     "POPOVER", | ||||
|     "RECOLOR", | ||||
|     "CUSTOM_1", | ||||
|     "CUSTOM_2", | ||||
| ) | ||||
|  | ||||
| LV_BASE_ALIGNMENTS = ( | ||||
|     "START", | ||||
|     "CENTER", | ||||
|     "END", | ||||
| ) | ||||
| LV_CELL_ALIGNMENTS = LvConstant( | ||||
|     "LV_GRID_ALIGN_", | ||||
|     *LV_BASE_ALIGNMENTS, | ||||
| ) | ||||
| LV_GRID_ALIGNMENTS = LV_CELL_ALIGNMENTS.extend( | ||||
|     "STRETCH", | ||||
|     "SPACE_EVENLY", | ||||
|     "SPACE_AROUND", | ||||
|     "SPACE_BETWEEN", | ||||
| ) | ||||
|  | ||||
| LV_FLEX_ALIGNMENTS = LvConstant( | ||||
|     "LV_FLEX_ALIGN_", | ||||
|     *LV_BASE_ALIGNMENTS, | ||||
|     "SPACE_EVENLY", | ||||
|     "SPACE_AROUND", | ||||
|     "SPACE_BETWEEN", | ||||
| ) | ||||
|  | ||||
| LV_MENU_MODES = LvConstant( | ||||
|     "LV_MENU_HEADER_", | ||||
|     "TOP_FIXED", | ||||
|     "TOP_UNFIXED", | ||||
|     "BOTTOM_FIXED", | ||||
| ) | ||||
|  | ||||
| LV_CHART_TYPES = ( | ||||
|     "NONE", | ||||
|     "LINE", | ||||
|     "BAR", | ||||
|     "SCATTER", | ||||
| ) | ||||
| LV_CHART_AXES = ( | ||||
|     "PRIMARY_Y", | ||||
|     "SECONDARY_Y", | ||||
|     "PRIMARY_X", | ||||
|     "SECONDARY_X", | ||||
| ) | ||||
|  | ||||
| CONF_ACCEPTED_CHARS = "accepted_chars" | ||||
| CONF_ADJUSTABLE = "adjustable" | ||||
| CONF_ALIGN = "align" | ||||
| CONF_ALIGN_TO = "align_to" | ||||
| CONF_ANGLE_RANGE = "angle_range" | ||||
| CONF_ANIMATED = "animated" | ||||
| CONF_ANIMATION = "animation" | ||||
| CONF_ANTIALIAS = "antialias" | ||||
| CONF_ARC_LENGTH = "arc_length" | ||||
| CONF_AUTO_START = "auto_start" | ||||
| CONF_BACKGROUND_STYLE = "background_style" | ||||
| CONF_DECIMAL_PLACES = "decimal_places" | ||||
| CONF_COLUMN = "column" | ||||
| CONF_DIGITS = "digits" | ||||
| CONF_DISP_BG_COLOR = "disp_bg_color" | ||||
| CONF_DISP_BG_IMAGE = "disp_bg_image" | ||||
| CONF_BODY = "body" | ||||
| CONF_BUTTONS = "buttons" | ||||
| CONF_BYTE_ORDER = "byte_order" | ||||
| CONF_CHANGE_RATE = "change_rate" | ||||
| CONF_CLOSE_BUTTON = "close_button" | ||||
| CONF_COLOR_DEPTH = "color_depth" | ||||
| CONF_COLOR_END = "color_end" | ||||
| CONF_COLOR_START = "color_start" | ||||
| CONF_CONTROL = "control" | ||||
| CONF_DEFAULT = "default" | ||||
| CONF_DEFAULT_FONT = "default_font" | ||||
| CONF_DIR = "dir" | ||||
| CONF_DISPLAYS = "displays" | ||||
| CONF_END_ANGLE = "end_angle" | ||||
| CONF_END_VALUE = "end_value" | ||||
| CONF_ENTER_BUTTON = "enter_button" | ||||
| CONF_ENTRIES = "entries" | ||||
| CONF_FLAGS = "flags" | ||||
| CONF_FLEX_FLOW = "flex_flow" | ||||
| CONF_FLEX_ALIGN_MAIN = "flex_align_main" | ||||
| CONF_FLEX_ALIGN_CROSS = "flex_align_cross" | ||||
| CONF_FLEX_ALIGN_TRACK = "flex_align_track" | ||||
| CONF_FLEX_GROW = "flex_grow" | ||||
| CONF_FULL_REFRESH = "full_refresh" | ||||
| CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos" | ||||
| CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos" | ||||
| CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span" | ||||
| CONF_GRID_CELL_COLUMN_SPAN = "grid_cell_column_span" | ||||
| CONF_GRID_CELL_X_ALIGN = "grid_cell_x_align" | ||||
| CONF_GRID_CELL_Y_ALIGN = "grid_cell_y_align" | ||||
| CONF_GRID_COLUMN_ALIGN = "grid_column_align" | ||||
| CONF_GRID_COLUMNS = "grid_columns" | ||||
| CONF_GRID_ROW_ALIGN = "grid_row_align" | ||||
| CONF_GRID_ROWS = "grid_rows" | ||||
| CONF_HEADER_MODE = "header_mode" | ||||
| CONF_HOME = "home" | ||||
| CONF_INDICATORS = "indicators" | ||||
| CONF_KEY_CODE = "key_code" | ||||
| CONF_LABEL_GAP = "label_gap" | ||||
| CONF_LAYOUT = "layout" | ||||
| CONF_LEFT_BUTTON = "left_button" | ||||
| CONF_LINE_WIDTH = "line_width" | ||||
| CONF_LOG_LEVEL = "log_level" | ||||
| CONF_LONG_PRESS_TIME = "long_press_time" | ||||
| CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" | ||||
| CONF_LVGL_ID = "lvgl_id" | ||||
| CONF_LONG_MODE = "long_mode" | ||||
| CONF_MAJOR = "major" | ||||
| CONF_MSGBOXES = "msgboxes" | ||||
| CONF_OBJ = "obj" | ||||
| CONF_OFFSET_X = "offset_x" | ||||
| CONF_OFFSET_Y = "offset_y" | ||||
| CONF_ONE_LINE = "one_line" | ||||
| CONF_ON_SELECT = "on_select" | ||||
| CONF_ONE_CHECKED = "one_checked" | ||||
| CONF_NEXT = "next" | ||||
| CONF_PAGE_WRAP = "page_wrap" | ||||
| CONF_PASSWORD_MODE = "password_mode" | ||||
| CONF_PIVOT_X = "pivot_x" | ||||
| CONF_PIVOT_Y = "pivot_y" | ||||
| CONF_PLACEHOLDER_TEXT = "placeholder_text" | ||||
| CONF_POINTS = "points" | ||||
| CONF_PREVIOUS = "previous" | ||||
| CONF_REPEAT_COUNT = "repeat_count" | ||||
| CONF_R_MOD = "r_mod" | ||||
| CONF_RECOLOR = "recolor" | ||||
| CONF_RIGHT_BUTTON = "right_button" | ||||
| CONF_ROLLOVER = "rollover" | ||||
| CONF_ROOT_BACK_BTN = "root_back_btn" | ||||
| CONF_ROWS = "rows" | ||||
| CONF_SCALES = "scales" | ||||
| CONF_SCALE_LINES = "scale_lines" | ||||
| CONF_SCROLLBAR_MODE = "scrollbar_mode" | ||||
| CONF_SELECTED_INDEX = "selected_index" | ||||
| CONF_SHOW_SNOW = "show_snow" | ||||
| CONF_SPIN_TIME = "spin_time" | ||||
| CONF_SRC = "src" | ||||
| CONF_START_ANGLE = "start_angle" | ||||
| CONF_START_VALUE = "start_value" | ||||
| CONF_STATES = "states" | ||||
| CONF_STRIDE = "stride" | ||||
| CONF_STYLE = "style" | ||||
| CONF_STYLE_ID = "style_id" | ||||
| CONF_SKIP = "skip" | ||||
| CONF_SYMBOL = "symbol" | ||||
| CONF_TAB_ID = "tab_id" | ||||
| CONF_TABS = "tabs" | ||||
| CONF_TEXT = "text" | ||||
| CONF_TILE = "tile" | ||||
| CONF_TILE_ID = "tile_id" | ||||
| CONF_TILES = "tiles" | ||||
| CONF_TITLE = "title" | ||||
| CONF_TOP_LAYER = "top_layer" | ||||
| CONF_TRANSPARENCY_KEY = "transparency_key" | ||||
| CONF_THEME = "theme" | ||||
| CONF_VISIBLE_ROW_COUNT = "visible_row_count" | ||||
| CONF_WIDGET = "widget" | ||||
| CONF_WIDGETS = "widgets" | ||||
| CONF_X = "x" | ||||
| CONF_Y = "y" | ||||
| CONF_ZOOM = "zoom" | ||||
|  | ||||
| # Keypad keys | ||||
|  | ||||
| LV_KEYS = LvConstant( | ||||
|     "LV_KEY_", | ||||
|     "UP", | ||||
|     "DOWN", | ||||
|     "RIGHT", | ||||
|     "LEFT", | ||||
|     "ESC", | ||||
|     "DEL", | ||||
|     "BACKSPACE", | ||||
|     "ENTER", | ||||
|     "NEXT", | ||||
|     "PREV", | ||||
|     "HOME", | ||||
|     "END", | ||||
| ) | ||||
|  | ||||
|  | ||||
| # list of widgets and the parts allowed | ||||
| WIDGET_PARTS = { | ||||
|     CONF_LABEL: (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED), | ||||
|     CONF_OBJ: (CONF_MAIN,), | ||||
| } | ||||
|  | ||||
| DEFAULT_ESPHOME_FONT = "esphome_lv_default_font" | ||||
|  | ||||
|  | ||||
| def join_enums(enums, prefix=""): | ||||
|     return "|".join(f"(int){prefix}{e.upper()}" for e in enums) | ||||
							
								
								
									
										76
									
								
								esphome/components/lvgl/font.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/lvgl/font.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| #include "lvgl_esphome.h" | ||||
|  | ||||
| #ifdef USE_LVGL_FONT | ||||
| namespace esphome { | ||||
| namespace lvgl { | ||||
|  | ||||
| static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { | ||||
|   auto *fe = (FontEngine *) font->dsc; | ||||
|   const auto *gd = fe->get_glyph_data(unicode_letter); | ||||
|   if (gd == nullptr) | ||||
|     return nullptr; | ||||
|   // esph_log_d(TAG, "Returning bitmap @  %X", (uint32_t)gd->data); | ||||
|  | ||||
|   return gd->data; | ||||
| } | ||||
|  | ||||
| static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { | ||||
|   auto *fe = (FontEngine *) font->dsc; | ||||
|   const auto *gd = fe->get_glyph_data(unicode_letter); | ||||
|   if (gd == nullptr) | ||||
|     return false; | ||||
|   dsc->adv_w = gd->offset_x + gd->width; | ||||
|   dsc->ofs_x = gd->offset_x; | ||||
|   dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline; | ||||
|   dsc->box_w = gd->width; | ||||
|   dsc->box_h = gd->height; | ||||
|   dsc->is_placeholder = 0; | ||||
|   dsc->bpp = fe->bpp; | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) { | ||||
|   this->bpp = esp_font->get_bpp(); | ||||
|   this->lv_font_.dsc = this; | ||||
|   this->lv_font_.line_height = this->height = esp_font->get_height(); | ||||
|   this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline(); | ||||
|   this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb; | ||||
|   this->lv_font_.get_glyph_bitmap = get_glyph_bitmap; | ||||
|   this->lv_font_.subpx = LV_FONT_SUBPX_NONE; | ||||
|   this->lv_font_.underline_position = -1; | ||||
|   this->lv_font_.underline_thickness = 1; | ||||
| } | ||||
|  | ||||
| const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; } | ||||
|  | ||||
| const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) { | ||||
|   if (unicode_letter == last_letter_) | ||||
|     return this->last_data_; | ||||
|   uint8_t unicode[5]; | ||||
|   memset(unicode, 0, sizeof unicode); | ||||
|   if (unicode_letter > 0xFFFF) { | ||||
|     unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7); | ||||
|     unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F); | ||||
|     unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F); | ||||
|     unicode[3] = 0x80 + (unicode_letter & 0x3F); | ||||
|   } else if (unicode_letter > 0x7FF) { | ||||
|     unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF); | ||||
|     unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F); | ||||
|     unicode[2] = 0x80 + (unicode_letter & 0x3F); | ||||
|   } else if (unicode_letter > 0x7F) { | ||||
|     unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F); | ||||
|     unicode[1] = 0x80 + (unicode_letter & 0x3F); | ||||
|   } else { | ||||
|     unicode[0] = unicode_letter; | ||||
|   } | ||||
|   int match_length; | ||||
|   int glyph_n = this->font_->match_next_glyph(unicode, &match_length); | ||||
|   if (glyph_n < 0) | ||||
|     return nullptr; | ||||
|   this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data(); | ||||
|   this->last_letter_ = unicode_letter; | ||||
|   return this->last_data_; | ||||
| } | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
| #endif  // USES_LVGL_FONT | ||||
							
								
								
									
										70
									
								
								esphome/components/lvgl/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								esphome/components/lvgl/helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| import re | ||||
|  | ||||
| from esphome import config_validation as cv | ||||
| from esphome.config import Config | ||||
| from esphome.const import CONF_ARGS, CONF_FORMAT | ||||
| from esphome.core import CORE, ID | ||||
| from esphome.yaml_util import ESPHomeDataBase | ||||
|  | ||||
| lv_uses = { | ||||
|     "USER_DATA", | ||||
|     "LOG", | ||||
|     "STYLE", | ||||
|     "FONT_PLACEHOLDER", | ||||
|     "THEME_DEFAULT", | ||||
| } | ||||
|  | ||||
|  | ||||
| def add_lv_use(*names): | ||||
|     for name in names: | ||||
|         lv_uses.add(name) | ||||
|  | ||||
|  | ||||
| lv_fonts_used = set() | ||||
| esphome_fonts_used = set() | ||||
| REQUIRED_COMPONENTS = {} | ||||
| lvgl_components_required = set() | ||||
|  | ||||
|  | ||||
| def validate_printf(value): | ||||
|     cfmt = r""" | ||||
|     (                                  # start of capture group 1 | ||||
|     %                                  # literal "%" | ||||
|     (?:[-+0 #]{0,5})                   # optional flags | ||||
|     (?:\d+|\*)?                        # width | ||||
|     (?:\.(?:\d+|\*))?                  # precision | ||||
|     (?:h|l|ll|w|I|I32|I64)?            # size | ||||
|     [cCdiouxXeEfgGaAnpsSZ]             # type | ||||
|     ) | ||||
|     """  # noqa | ||||
|     matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X) | ||||
|     if len(matches) != len(value[CONF_ARGS]): | ||||
|         raise cv.Invalid( | ||||
|             f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" | ||||
|         ) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def get_line_marks(value) -> list: | ||||
|     """ | ||||
|     If possible, return a preprocessor directive to identify the line number where the given id was defined. | ||||
|     :param id: The id in question | ||||
|     :return: A list containing zero or more line directives | ||||
|     """ | ||||
|     path = None | ||||
|     if isinstance(value, ESPHomeDataBase): | ||||
|         path = value.esp_range | ||||
|     elif isinstance(value, ID) and isinstance(CORE.config, Config): | ||||
|         path = CORE.config.get_path_for_id(value)[:-1] | ||||
|         path = CORE.config.get_deepest_document_range_for_path(path) | ||||
|     if path is None: | ||||
|         return [] | ||||
|     return [path.start_mark.as_line_directive] | ||||
|  | ||||
|  | ||||
| def requires_component(comp): | ||||
|     def validator(value): | ||||
|         lvgl_components_required.add(comp) | ||||
|         return cv.requires_component(comp)(value) | ||||
|  | ||||
|     return validator | ||||
							
								
								
									
										34
									
								
								esphome/components/lvgl/label.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/lvgl/label.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from .defines import CONF_LABEL, CONF_LONG_MODE, CONF_RECOLOR, CONF_TEXT, LV_LONG_MODES | ||||
| from .lv_validation import lv_bool, lv_text | ||||
| from .schemas import TEXT_SCHEMA | ||||
| from .types import lv_label_t | ||||
| from .widget import Widget, WidgetType | ||||
|  | ||||
|  | ||||
| class LabelType(WidgetType): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             CONF_LABEL, | ||||
|             TEXT_SCHEMA.extend( | ||||
|                 { | ||||
|                     cv.Optional(CONF_RECOLOR): lv_bool, | ||||
|                     cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, | ||||
|                 } | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def w_type(self): | ||||
|         return lv_label_t | ||||
|  | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         """For a text object, create and set text""" | ||||
|         if value := config.get(CONF_TEXT): | ||||
|             w.set_property(CONF_TEXT, await lv_text.process(value)) | ||||
|         w.set_property(CONF_LONG_MODE, config) | ||||
|         w.set_property(CONF_RECOLOR, config) | ||||
|  | ||||
|  | ||||
| label_spec = LabelType() | ||||
							
								
								
									
										170
									
								
								esphome/components/lvgl/lv_validation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								esphome/components/lvgl/lv_validation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.binary_sensor import BinarySensor | ||||
| from esphome.components.color import ColorStruct | ||||
| from esphome.components.font import Font | ||||
| from esphome.components.sensor import Sensor | ||||
| from esphome.components.text_sensor import TextSensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT | ||||
| from esphome.core import HexInt | ||||
| from esphome.cpp_generator import MockObj | ||||
| from esphome.helpers import cpp_string_escape | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
|  | ||||
| from . import types as ty | ||||
| from .defines import LV_FONTS, LValidator, LvConstant | ||||
| from .helpers import ( | ||||
|     esphome_fonts_used, | ||||
|     lv_fonts_used, | ||||
|     lvgl_components_required, | ||||
|     requires_component, | ||||
| ) | ||||
| from .lvcode import ConstantLiteral, lv_expr | ||||
| from .types import lv_font_t | ||||
|  | ||||
|  | ||||
| @schema_extractor("one_of") | ||||
| def color(value): | ||||
|     if value == SCHEMA_EXTRACT: | ||||
|         return ["hex color value", "color ID"] | ||||
|     if isinstance(value, int): | ||||
|         return value | ||||
|     return cv.use_id(ColorStruct)(value) | ||||
|  | ||||
|  | ||||
| def color_retmapper(value): | ||||
|     if isinstance(value, cv.Lambda): | ||||
|         return cv.returning_lambda(value) | ||||
|     if isinstance(value, int): | ||||
|         hexval = HexInt(value) | ||||
|         return lv_expr.color_hex(hexval) | ||||
|     # Must be an id | ||||
|     lvgl_components_required.add(CONF_COLOR) | ||||
|     return lv_expr.color_from(MockObj(value)) | ||||
|  | ||||
|  | ||||
| def pixels_or_percent(value): | ||||
|     """A length in one axis - either a number (pixels) or a percentage""" | ||||
|     if value == SCHEMA_EXTRACT: | ||||
|         return ["pixels", "..%"] | ||||
|     if isinstance(value, int): | ||||
|         return str(cv.int_(value)) | ||||
|     # Will throw an exception if not a percentage. | ||||
|     return f"lv_pct({int(cv.percentage(value) * 100)})" | ||||
|  | ||||
|  | ||||
| def zoom(value): | ||||
|     value = cv.float_range(0.1, 10.0)(value) | ||||
|     return int(value * 256) | ||||
|  | ||||
|  | ||||
| def angle(value): | ||||
|     """ | ||||
|     Validation for an angle in degrees, converted to an integer representing 0.1deg units | ||||
|     :param value: The input in the range 0..360 | ||||
|     :return: An angle in 1/10 degree units. | ||||
|     """ | ||||
|     return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10) | ||||
|  | ||||
|  | ||||
| @schema_extractor("one_of") | ||||
| def size(value): | ||||
|     """A size in one axis - one of "size_content", a number (pixels) or a percentage""" | ||||
|     if value == SCHEMA_EXTRACT: | ||||
|         return ["size_content", "pixels", "..%"] | ||||
|     if isinstance(value, str) and value.lower().endswith("px"): | ||||
|         value = cv.int_(value[:-2]) | ||||
|     if isinstance(value, str) and not value.endswith("%"): | ||||
|         if value.upper() == "SIZE_CONTENT": | ||||
|             return "LV_SIZE_CONTENT" | ||||
|         raise cv.Invalid("must be 'size_content', a pixel position or a percentage") | ||||
|     if isinstance(value, int): | ||||
|         return str(cv.int_(value)) | ||||
|     # Will throw an exception if not a percentage. | ||||
|     return f"lv_pct({int(cv.percentage(value) * 100)})" | ||||
|  | ||||
|  | ||||
| @schema_extractor("one_of") | ||||
| def opacity(value): | ||||
|     consts = LvConstant("LV_OPA_", "TRANSP", "COVER") | ||||
|     if value == SCHEMA_EXTRACT: | ||||
|         return consts.choices | ||||
|     value = cv.Any(cv.percentage, consts.one_of)(value) | ||||
|     if isinstance(value, float): | ||||
|         return int(value * 255) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def stop_value(value): | ||||
|     return cv.int_range(0, 255)(value) | ||||
|  | ||||
|  | ||||
| lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) | ||||
| lv_bool = LValidator(cv.boolean, cg.bool_, BinarySensor, "get_state()") | ||||
|  | ||||
|  | ||||
| def lvms_validator_(value): | ||||
|     if value == "never": | ||||
|         value = "2147483647ms" | ||||
|     return cv.positive_time_period_milliseconds(value) | ||||
|  | ||||
|  | ||||
| lv_milliseconds = LValidator( | ||||
|     lvms_validator_, | ||||
|     cg.int32, | ||||
|     retmapper=lambda x: x.total_milliseconds, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class TextValidator(LValidator): | ||||
|     def __init__(self): | ||||
|         super().__init__( | ||||
|             cv.string, | ||||
|             cg.const_char_ptr, | ||||
|             TextSensor, | ||||
|             "get_state().c_str()", | ||||
|             lambda s: cg.safe_exp(f"{s}"), | ||||
|         ) | ||||
|  | ||||
|     def __call__(self, value): | ||||
|         if isinstance(value, dict): | ||||
|             return value | ||||
|         return super().__call__(value) | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|         if isinstance(value, dict): | ||||
|             args = [str(x) for x in value[CONF_ARGS]] | ||||
|             arg_expr = cg.RawExpression(",".join(args)) | ||||
|             format_str = cpp_string_escape(value[CONF_FORMAT]) | ||||
|             return f"str_sprintf({format_str}, {arg_expr}).c_str()" | ||||
|         return await super().process(value, args) | ||||
|  | ||||
|  | ||||
| lv_text = TextValidator() | ||||
| lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") | ||||
| lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") | ||||
|  | ||||
|  | ||||
| class LvFont(LValidator): | ||||
|     def __init__(self): | ||||
|         def lv_builtin_font(value): | ||||
|             fontval = cv.one_of(*LV_FONTS, lower=True)(value) | ||||
|             lv_fonts_used.add(fontval) | ||||
|             return "&lv_font_" + fontval | ||||
|  | ||||
|         def validator(value): | ||||
|             if value == SCHEMA_EXTRACT: | ||||
|                 return LV_FONTS | ||||
|             if isinstance(value, str) and value.lower() in LV_FONTS: | ||||
|                 return lv_builtin_font(value) | ||||
|             fontval = cv.use_id(Font)(value) | ||||
|             esphome_fonts_used.add(fontval) | ||||
|             return requires_component("font")(f"{fontval}_engine->get_lv_font()") | ||||
|  | ||||
|         super().__init__(validator, lv_font_t) | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|         return ConstantLiteral(value) | ||||
|  | ||||
|  | ||||
| lv_font = LvFont() | ||||
							
								
								
									
										237
									
								
								esphome/components/lvgl/lvcode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								esphome/components/lvgl/lvcode.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | ||||
| import abc | ||||
| import logging | ||||
| from typing import Union | ||||
|  | ||||
| from esphome import codegen as cg | ||||
| from esphome.core import ID, Lambda | ||||
| from esphome.cpp_generator import ( | ||||
|     AssignmentExpression, | ||||
|     CallExpression, | ||||
|     Expression, | ||||
|     LambdaExpression, | ||||
|     Literal, | ||||
|     MockObj, | ||||
|     RawExpression, | ||||
|     RawStatement, | ||||
|     SafeExpType, | ||||
|     Statement, | ||||
|     VariableDeclarationExpression, | ||||
|     statement, | ||||
| ) | ||||
|  | ||||
| from .helpers import get_line_marks | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class CodeContext(abc.ABC): | ||||
|     """ | ||||
|     A class providing a context for code generation. Generated code will be added to the | ||||
|     current context. A new context will stack on the current context, and restore it | ||||
|     when done. Used with the `with` statement. | ||||
|     """ | ||||
|  | ||||
|     code_context = None | ||||
|  | ||||
|     @abc.abstractmethod | ||||
|     def add(self, expression: Union[Expression, Statement]): | ||||
|         pass | ||||
|  | ||||
|     @staticmethod | ||||
|     def append(expression: Union[Expression, Statement]): | ||||
|         if CodeContext.code_context is not None: | ||||
|             CodeContext.code_context.add(expression) | ||||
|         return expression | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.previous: Union[CodeContext | None] = None | ||||
|  | ||||
|     def __enter__(self): | ||||
|         self.previous = CodeContext.code_context | ||||
|         CodeContext.code_context = self | ||||
|  | ||||
|     def __exit__(self, *args): | ||||
|         CodeContext.code_context = self.previous | ||||
|  | ||||
|  | ||||
| class MainContext(CodeContext): | ||||
|     """ | ||||
|     Code generation into the main() function | ||||
|     """ | ||||
|  | ||||
|     def add(self, expression: Union[Expression, Statement]): | ||||
|         return cg.add(expression) | ||||
|  | ||||
|  | ||||
| class LvContext(CodeContext): | ||||
|     """ | ||||
|     Code generation into the LVGL initialisation code (called in `setup()`) | ||||
|     """ | ||||
|  | ||||
|     lv_init_code: list["Statement"] = [] | ||||
|  | ||||
|     @staticmethod | ||||
|     def lv_add(expression: Union[Expression, Statement]): | ||||
|         if isinstance(expression, Expression): | ||||
|             expression = statement(expression) | ||||
|         if not isinstance(expression, Statement): | ||||
|             raise ValueError( | ||||
|                 f"Add '{expression}' must be expression or statement, not {type(expression)}" | ||||
|             ) | ||||
|         LvContext.lv_init_code.append(expression) | ||||
|         _LOGGER.debug("LV Adding: %s", expression) | ||||
|         return expression | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_code(): | ||||
|         code = [] | ||||
|         for exp in LvContext.lv_init_code: | ||||
|             text = str(statement(exp)) | ||||
|             text = text.rstrip() | ||||
|             code.append(text) | ||||
|         return "\n".join(code) + "\n\n" | ||||
|  | ||||
|     def add(self, expression: Union[Expression, Statement]): | ||||
|         return LvContext.lv_add(expression) | ||||
|  | ||||
|     def set_style(self, prop): | ||||
|         return MockObj("lv_set_style_{prop}", "") | ||||
|  | ||||
|  | ||||
| class LambdaContext(CodeContext): | ||||
|     """ | ||||
|     A context that will accumlate code for use in a lambda. | ||||
|     """ | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         parameters: list[tuple[SafeExpType, str]], | ||||
|         return_type: SafeExpType = None, | ||||
|     ): | ||||
|         super().__init__() | ||||
|         self.code_list: list[Statement] = [] | ||||
|         self.parameters = parameters | ||||
|         self.return_type = return_type | ||||
|  | ||||
|     def add(self, expression: Union[Expression, Statement]): | ||||
|         self.code_list.append(expression) | ||||
|         return expression | ||||
|  | ||||
|     async def code(self) -> LambdaExpression: | ||||
|         code_text = [] | ||||
|         for exp in self.code_list: | ||||
|             text = str(statement(exp)) | ||||
|             text = text.rstrip() | ||||
|             code_text.append(text) | ||||
|         return await cg.process_lambda( | ||||
|             Lambda("\n".join(code_text) + "\n\n"), | ||||
|             self.parameters, | ||||
|             return_type=self.return_type, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class LocalVariable(MockObj): | ||||
|     """ | ||||
|     Create a local variable and enclose the code using it within a block. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, name, type, modifier=None, rhs=None): | ||||
|         base = ID(name, True, type) | ||||
|         super().__init__(base, "") | ||||
|         self.modifier = modifier | ||||
|         self.rhs = rhs | ||||
|  | ||||
|     def __enter__(self): | ||||
|         CodeContext.append(RawStatement("{")) | ||||
|         CodeContext.append( | ||||
|             VariableDeclarationExpression(self.base.type, self.modifier, self.base.id) | ||||
|         ) | ||||
|         if self.rhs is not None: | ||||
|             CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs)) | ||||
|         return self.base | ||||
|  | ||||
|     def __exit__(self, *args): | ||||
|         CodeContext.append(RawStatement("}")) | ||||
|  | ||||
|  | ||||
| class MockLv: | ||||
|     """ | ||||
|     A mock object that can be used to generate LVGL calls. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, base): | ||||
|         self.base = base | ||||
|  | ||||
|     def __getattr__(self, attr: str) -> "MockLv": | ||||
|         return MockLv(f"{self.base}{attr}") | ||||
|  | ||||
|     def append(self, expression): | ||||
|         CodeContext.append(expression) | ||||
|  | ||||
|     def __call__(self, *args: SafeExpType) -> "MockObj": | ||||
|         call = CallExpression(self.base, *args) | ||||
|         result = MockObj(call, "") | ||||
|         self.append(result) | ||||
|         return result | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(self.base) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return f"MockLv<{str(self.base)}>" | ||||
|  | ||||
|     def call(self, prop, *args): | ||||
|         call = CallExpression(RawExpression(f"{self.base}{prop}"), *args) | ||||
|         result = MockObj(call, "") | ||||
|         self.append(result) | ||||
|         return result | ||||
|  | ||||
|     def cond_if(self, expression: Expression): | ||||
|         CodeContext.append(RawExpression(f"if({expression}) {{")) | ||||
|  | ||||
|     def cond_else(self): | ||||
|         CodeContext.append(RawExpression("} else {")) | ||||
|  | ||||
|     def cond_endif(self): | ||||
|         CodeContext.append(RawExpression("}")) | ||||
|  | ||||
|  | ||||
| class LvExpr(MockLv): | ||||
|     def __getattr__(self, attr: str) -> "MockLv": | ||||
|         return LvExpr(f"{self.base}{attr}") | ||||
|  | ||||
|     def append(self, expression): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| # Top level mock for generic lv_ calls to be recorded | ||||
| lv = MockLv("lv_") | ||||
| # Just generate an expression | ||||
| lv_expr = LvExpr("lv_") | ||||
| # Mock for lv_obj_ calls | ||||
| lv_obj = MockLv("lv_obj_") | ||||
|  | ||||
|  | ||||
| # equivalent to cg.add() for the lvgl init context | ||||
| def lv_add(expression: Union[Expression, Statement]): | ||||
|     return CodeContext.append(expression) | ||||
|  | ||||
|  | ||||
| def add_line_marks(where): | ||||
|     for mark in get_line_marks(where): | ||||
|         lv_add(cg.RawStatement(mark)) | ||||
|  | ||||
|  | ||||
| def lv_assign(target, expression): | ||||
|     lv_add(RawExpression(f"{target} = {expression}")) | ||||
|  | ||||
|  | ||||
| class ConstantLiteral(Literal): | ||||
|     __slots__ = ("constant",) | ||||
|  | ||||
|     def __init__(self, constant: str): | ||||
|         super().__init__() | ||||
|         self.constant = constant | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.constant | ||||
							
								
								
									
										129
									
								
								esphome/components/lvgl/lvgl_esphome.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								esphome/components/lvgl/lvgl_esphome.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "lvgl_hal.h" | ||||
| #include "lvgl_esphome.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace lvgl { | ||||
| static const char *const TAG = "lvgl"; | ||||
|  | ||||
| lv_event_code_t lv_custom_event;  // NOLINT | ||||
| void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); } | ||||
| void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) { | ||||
|   for (auto *display : this->displays_) { | ||||
|     display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr, | ||||
|                             display::COLOR_ORDER_RGB, LV_BITNESS, LV_COLOR_16_SWAP); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { | ||||
|   auto now = millis(); | ||||
|   this->draw_buffer_(area, (const uint8_t *) color_p); | ||||
|   ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), | ||||
|            lv_area_get_height(area), (int) (millis() - now)); | ||||
|   lv_disp_flush_ready(disp_drv); | ||||
| } | ||||
|  | ||||
| void LvglComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "LVGL Setup starts"); | ||||
| #if LV_USE_LOG | ||||
|   lv_log_register_print_cb(log_cb); | ||||
| #endif | ||||
|   lv_init(); | ||||
|   lv_custom_event = static_cast<lv_event_code_t>(lv_event_register_id()); | ||||
|   auto *display = this->displays_[0]; | ||||
|   size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_; | ||||
|   auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; | ||||
|   auto *buf = lv_custom_mem_alloc(buf_bytes); | ||||
|   if (buf == nullptr) { | ||||
|     ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes); | ||||
|     this->mark_failed(); | ||||
|     this->status_set_error("Memory allocation failure"); | ||||
|     return; | ||||
|   } | ||||
|   lv_disp_draw_buf_init(&this->draw_buf_, buf, nullptr, buffer_pixels); | ||||
|   lv_disp_drv_init(&this->disp_drv_); | ||||
|   this->disp_drv_.draw_buf = &this->draw_buf_; | ||||
|   this->disp_drv_.user_data = this; | ||||
|   this->disp_drv_.full_refresh = this->full_refresh_; | ||||
|   this->disp_drv_.flush_cb = static_flush_cb; | ||||
|   this->disp_drv_.rounder_cb = rounder_cb; | ||||
|   switch (display->get_rotation()) { | ||||
|     case display::DISPLAY_ROTATION_0_DEGREES: | ||||
|       break; | ||||
|     case display::DISPLAY_ROTATION_90_DEGREES: | ||||
|       this->disp_drv_.sw_rotate = true; | ||||
|       this->disp_drv_.rotated = LV_DISP_ROT_90; | ||||
|       break; | ||||
|     case display::DISPLAY_ROTATION_180_DEGREES: | ||||
|       this->disp_drv_.sw_rotate = true; | ||||
|       this->disp_drv_.rotated = LV_DISP_ROT_180; | ||||
|       break; | ||||
|     case display::DISPLAY_ROTATION_270_DEGREES: | ||||
|       this->disp_drv_.sw_rotate = true; | ||||
|       this->disp_drv_.rotated = LV_DISP_ROT_270; | ||||
|       break; | ||||
|   } | ||||
|   display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); | ||||
|   this->disp_drv_.hor_res = (lv_coord_t) display->get_width(); | ||||
|   this->disp_drv_.ver_res = (lv_coord_t) display->get_height(); | ||||
|   ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated); | ||||
|   this->disp_ = lv_disp_drv_register(&this->disp_drv_); | ||||
|   for (const auto &v : this->init_lambdas_) | ||||
|     v(this->disp_); | ||||
|   lv_disp_trig_activity(this->disp_); | ||||
|   ESP_LOGCONFIG(TAG, "LVGL Setup complete"); | ||||
| } | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
|  | ||||
| size_t lv_millis(void) { return esphome::millis(); } | ||||
|  | ||||
| #if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266) | ||||
| void *lv_custom_mem_alloc(size_t size) { | ||||
|   auto *ptr = malloc(size);  // NOLINT | ||||
|   if (ptr == nullptr) { | ||||
|     esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); | ||||
|   } | ||||
|   return ptr; | ||||
| } | ||||
| void lv_custom_mem_free(void *ptr) { return free(ptr); }                            // NOLINT | ||||
| void *lv_custom_mem_realloc(void *ptr, size_t size) { return realloc(ptr, size); }  // NOLINT | ||||
| #else | ||||
| static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT;  // NOLINT | ||||
|  | ||||
| void *lv_custom_mem_alloc(size_t size) { | ||||
|   void *ptr; | ||||
|   ptr = heap_caps_malloc(size, cap_bits); | ||||
|   if (ptr == nullptr) { | ||||
|     cap_bits = MALLOC_CAP_8BIT; | ||||
|     ptr = heap_caps_malloc(size, cap_bits); | ||||
|   } | ||||
|   if (ptr == nullptr) { | ||||
|     esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); | ||||
|     return nullptr; | ||||
|   } | ||||
| #ifdef ESPHOME_LOG_HAS_VERBOSE | ||||
|   esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr); | ||||
| #endif | ||||
|   return ptr; | ||||
| } | ||||
|  | ||||
| void lv_custom_mem_free(void *ptr) { | ||||
| #ifdef ESPHOME_LOG_HAS_VERBOSE | ||||
|   esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); | ||||
| #endif | ||||
|   if (ptr == nullptr) | ||||
|     return; | ||||
|   heap_caps_free(ptr); | ||||
| } | ||||
|  | ||||
| void *lv_custom_mem_realloc(void *ptr, size_t size) { | ||||
| #ifdef ESPHOME_LOG_HAS_VERBOSE | ||||
|   esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); | ||||
| #endif | ||||
|   return heap_caps_realloc(ptr, size, cap_bits); | ||||
| } | ||||
| #endif | ||||
							
								
								
									
										119
									
								
								esphome/components/lvgl/lvgl_esphome.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								esphome/components/lvgl/lvgl_esphome.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| #pragma once | ||||
| #include "esphome/core/defines.h" | ||||
| #ifdef USE_LVGL | ||||
|  | ||||
| // required for clang-tidy | ||||
| #ifndef LV_CONF_H | ||||
| #define LV_CONF_SKIP 1  // NOLINT | ||||
| #endif | ||||
|  | ||||
| #include "esphome/components/display/display.h" | ||||
| #include "esphome/components/display/display_color_utils.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <lvgl.h> | ||||
| #include <vector> | ||||
|  | ||||
| #ifdef USE_LVGL_FONT | ||||
| #include "esphome/components/font/font.h" | ||||
| #endif | ||||
| namespace esphome { | ||||
| namespace lvgl { | ||||
|  | ||||
| extern lv_event_code_t lv_custom_event;  // NOLINT | ||||
| #ifdef USE_LVGL_COLOR | ||||
| static lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); } | ||||
| #endif | ||||
| #if LV_COLOR_DEPTH == 16 | ||||
| static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565; | ||||
| #elif LV_COLOR_DEPTH == 32 | ||||
| static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_888; | ||||
| #else | ||||
| static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332; | ||||
| #endif | ||||
|  | ||||
| // Parent class for things that wrap an LVGL object | ||||
| class LvCompound { | ||||
|  public: | ||||
|   virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } | ||||
|   lv_obj_t *obj{}; | ||||
| }; | ||||
|  | ||||
| using LvLambdaType = std::function<void(lv_obj_t *)>; | ||||
| using set_value_lambda_t = std::function<void(float)>; | ||||
| using event_callback_t = void(_lv_event_t *); | ||||
| using text_lambda_t = std::function<const char *()>; | ||||
|  | ||||
| #ifdef USE_LVGL_FONT | ||||
| class FontEngine { | ||||
|  public: | ||||
|   FontEngine(font::Font *esp_font); | ||||
|   const lv_font_t *get_lv_font(); | ||||
|  | ||||
|   const font::GlyphData *get_glyph_data(uint32_t unicode_letter); | ||||
|   uint16_t baseline{}; | ||||
|   uint16_t height{}; | ||||
|   uint8_t bpp{}; | ||||
|  | ||||
|  protected: | ||||
|   font::Font *font_{}; | ||||
|   uint32_t last_letter_{}; | ||||
|   const font::GlyphData *last_data_{}; | ||||
|   lv_font_t lv_font_{}; | ||||
| }; | ||||
| #endif  // USE_LVGL_FONT | ||||
|  | ||||
| class LvglComponent : public PollingComponent { | ||||
|   constexpr static const char *const TAG = "lvgl"; | ||||
|  | ||||
|  public: | ||||
|   static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { | ||||
|     reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); | ||||
|   } | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::PROCESSOR; } | ||||
|   static void log_cb(const char *buf) { | ||||
|     esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); | ||||
|   } | ||||
|   static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { | ||||
|     // make sure all coordinates are even | ||||
|     if (area->x1 & 1) | ||||
|       area->x1--; | ||||
|     if (!(area->x2 & 1)) | ||||
|       area->x2++; | ||||
|     if (area->y1 & 1) | ||||
|       area->y1--; | ||||
|     if (!(area->y2 & 1)) | ||||
|       area->y2++; | ||||
|   } | ||||
|  | ||||
|   void loop() override { lv_timer_handler_run_in_period(5); } | ||||
|   void setup() override; | ||||
|  | ||||
|   void update() override {} | ||||
|  | ||||
|   void add_display(display::Display *display) { this->displays_.push_back(display); } | ||||
|   void add_init_lambda(const std::function<void(lv_disp_t *)> &lamb) { this->init_lambdas_.push_back(lamb); } | ||||
|   void dump_config() override; | ||||
|   void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; } | ||||
|   void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; } | ||||
|   lv_disp_t *get_disp() { return this->disp_; } | ||||
|  | ||||
|  protected: | ||||
|   void draw_buffer_(const lv_area_t *area, const uint8_t *ptr); | ||||
|   void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); | ||||
|   std::vector<display::Display *> displays_{}; | ||||
|   lv_disp_draw_buf_t draw_buf_{}; | ||||
|   lv_disp_drv_t disp_drv_{}; | ||||
|   lv_disp_t *disp_{}; | ||||
|  | ||||
|   std::vector<std::function<void(lv_disp_t *)>> init_lambdas_; | ||||
|   size_t buffer_frac_{1}; | ||||
|   bool full_refresh_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace lvgl | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										21
									
								
								esphome/components/lvgl/lvgl_hal.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/lvgl/lvgl_hal.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| // | ||||
| // Created by Clyde Stubbs on 20/9/2023. | ||||
| // | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #ifdef __cplusplus | ||||
| #define EXTERNC extern "C" | ||||
| #include <cstddef> | ||||
| namespace esphome { | ||||
| namespace lvgl {} | ||||
| }  // namespace esphome | ||||
| #else | ||||
| #define EXTERNC extern | ||||
| #include <stddef.h> | ||||
| #endif | ||||
|  | ||||
| EXTERNC size_t lv_millis(void); | ||||
| EXTERNC void *lv_custom_mem_alloc(size_t size); | ||||
| EXTERNC void lv_custom_mem_free(void *ptr); | ||||
| EXTERNC void *lv_custom_mem_realloc(void *ptr, size_t size); | ||||
							
								
								
									
										22
									
								
								esphome/components/lvgl/obj.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								esphome/components/lvgl/obj.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| from .defines import CONF_OBJ | ||||
| from .types import lv_obj_t | ||||
| from .widget import WidgetType | ||||
|  | ||||
|  | ||||
| class ObjType(WidgetType): | ||||
|     """ | ||||
|     The base LVGL object. All other widgets inherit from this. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self): | ||||
|         super().__init__(CONF_OBJ, schema={}, modify_schema={}) | ||||
|  | ||||
|     @property | ||||
|     def w_type(self): | ||||
|         return lv_obj_t | ||||
|  | ||||
|     async def to_code(self, w, config): | ||||
|         return [] | ||||
|  | ||||
|  | ||||
| obj_spec = ObjType() | ||||
							
								
								
									
										260
									
								
								esphome/components/lvgl/schemas.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								esphome/components/lvgl/schemas.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,260 @@ | ||||
| from esphome import config_validation as cv | ||||
| from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT | ||||
|  | ||||
| from . import defines as df, lv_validation as lvalid, types as ty | ||||
| from .defines import WIDGET_PARTS | ||||
| from .helpers import ( | ||||
|     REQUIRED_COMPONENTS, | ||||
|     add_lv_use, | ||||
|     requires_component, | ||||
|     validate_printf, | ||||
| ) | ||||
| from .lv_validation import lv_font | ||||
| from .types import WIDGET_TYPES, get_widget_type | ||||
|  | ||||
| # A schema for text properties | ||||
| TEXT_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(df.CONF_TEXT): cv.Any( | ||||
|             cv.All( | ||||
|                 cv.Schema( | ||||
|                     { | ||||
|                         cv.Required(CONF_FORMAT): cv.string, | ||||
|                         cv.Optional(CONF_ARGS, default=list): cv.ensure_list( | ||||
|                             cv.lambda_ | ||||
|                         ), | ||||
|                     }, | ||||
|                 ), | ||||
|                 validate_printf, | ||||
|             ), | ||||
|             lvalid.lv_text, | ||||
|         ) | ||||
|     } | ||||
| ) | ||||
|  | ||||
| # All LVGL styles and their validators | ||||
| STYLE_PROPS = { | ||||
|     "align": df.CHILD_ALIGNMENTS.one_of, | ||||
|     "arc_opa": lvalid.opacity, | ||||
|     "arc_color": lvalid.lv_color, | ||||
|     "arc_rounded": lvalid.lv_bool, | ||||
|     "arc_width": cv.positive_int, | ||||
|     "anim_time": lvalid.lv_milliseconds, | ||||
|     "bg_color": lvalid.lv_color, | ||||
|     "bg_grad_color": lvalid.lv_color, | ||||
|     "bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of, | ||||
|     "bg_grad_dir": df.LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER").one_of, | ||||
|     "bg_grad_stop": lvalid.stop_value, | ||||
|     "bg_img_opa": lvalid.opacity, | ||||
|     "bg_img_recolor": lvalid.lv_color, | ||||
|     "bg_img_recolor_opa": lvalid.opacity, | ||||
|     "bg_main_stop": lvalid.stop_value, | ||||
|     "bg_opa": lvalid.opacity, | ||||
|     "border_color": lvalid.lv_color, | ||||
|     "border_opa": lvalid.opacity, | ||||
|     "border_post": lvalid.lv_bool, | ||||
|     "border_side": df.LvConstant( | ||||
|         "LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL" | ||||
|     ).several_of, | ||||
|     "border_width": cv.positive_int, | ||||
|     "clip_corner": lvalid.lv_bool, | ||||
|     "height": lvalid.size, | ||||
|     "img_recolor": lvalid.lv_color, | ||||
|     "img_recolor_opa": lvalid.opacity, | ||||
|     "line_width": cv.positive_int, | ||||
|     "line_dash_width": cv.positive_int, | ||||
|     "line_dash_gap": cv.positive_int, | ||||
|     "line_rounded": lvalid.lv_bool, | ||||
|     "line_color": lvalid.lv_color, | ||||
|     "opa": lvalid.opacity, | ||||
|     "opa_layered": lvalid.opacity, | ||||
|     "outline_color": lvalid.lv_color, | ||||
|     "outline_opa": lvalid.opacity, | ||||
|     "outline_pad": lvalid.size, | ||||
|     "outline_width": lvalid.size, | ||||
|     "pad_all": lvalid.size, | ||||
|     "pad_bottom": lvalid.size, | ||||
|     "pad_column": lvalid.size, | ||||
|     "pad_left": lvalid.size, | ||||
|     "pad_right": lvalid.size, | ||||
|     "pad_row": lvalid.size, | ||||
|     "pad_top": lvalid.size, | ||||
|     "shadow_color": lvalid.lv_color, | ||||
|     "shadow_ofs_x": cv.int_, | ||||
|     "shadow_ofs_y": cv.int_, | ||||
|     "shadow_opa": lvalid.opacity, | ||||
|     "shadow_spread": cv.int_, | ||||
|     "shadow_width": cv.positive_int, | ||||
|     "text_align": df.LvConstant( | ||||
|         "LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO" | ||||
|     ).one_of, | ||||
|     "text_color": lvalid.lv_color, | ||||
|     "text_decor": df.LvConstant( | ||||
|         "LV_TEXT_DECOR_", "NONE", "UNDERLINE", "STRIKETHROUGH" | ||||
|     ).several_of, | ||||
|     "text_font": lv_font, | ||||
|     "text_letter_space": cv.positive_int, | ||||
|     "text_line_space": cv.positive_int, | ||||
|     "text_opa": lvalid.opacity, | ||||
|     "transform_angle": lvalid.angle, | ||||
|     "transform_height": lvalid.pixels_or_percent, | ||||
|     "transform_pivot_x": lvalid.pixels_or_percent, | ||||
|     "transform_pivot_y": lvalid.pixels_or_percent, | ||||
|     "transform_zoom": lvalid.zoom, | ||||
|     "translate_x": lvalid.pixels_or_percent, | ||||
|     "translate_y": lvalid.pixels_or_percent, | ||||
|     "max_height": lvalid.pixels_or_percent, | ||||
|     "max_width": lvalid.pixels_or_percent, | ||||
|     "min_height": lvalid.pixels_or_percent, | ||||
|     "min_width": lvalid.pixels_or_percent, | ||||
|     "radius": cv.Any(lvalid.size, df.LvConstant("LV_RADIUS_", "CIRCLE").one_of), | ||||
|     "width": lvalid.size, | ||||
|     "x": lvalid.pixels_or_percent, | ||||
|     "y": lvalid.pixels_or_percent, | ||||
| } | ||||
|  | ||||
| # Complete object style schema | ||||
| STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( | ||||
|     { | ||||
|         cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( | ||||
|             "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" | ||||
|         ).one_of, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| # Object states. Top level properties apply to MAIN | ||||
| STATE_SCHEMA = cv.Schema( | ||||
|     {cv.Optional(state): STYLE_SCHEMA for state in df.STATES} | ||||
| ).extend(STYLE_SCHEMA) | ||||
| # Setting object states | ||||
| SET_STATE_SCHEMA = cv.Schema( | ||||
|     {cv.Optional(state): lvalid.lv_bool for state in df.STATES} | ||||
| ) | ||||
| # Setting object flags | ||||
| FLAG_SCHEMA = cv.Schema({cv.Optional(flag): cv.boolean for flag in df.OBJ_FLAGS}) | ||||
| FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of) | ||||
|  | ||||
|  | ||||
| def part_schema(widget_type): | ||||
|     """ | ||||
|     Generate a schema for the various parts (e.g. main:, indicator:) of a widget type | ||||
|     :param widget_type:  The type of widget to generate for | ||||
|     :return: | ||||
|     """ | ||||
|     parts = WIDGET_PARTS.get(widget_type) | ||||
|     if parts is None: | ||||
|         parts = (df.CONF_MAIN,) | ||||
|     return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend( | ||||
|         STATE_SCHEMA | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def obj_schema(widget_type: str): | ||||
|     """ | ||||
|     Create a schema for a widget type itself i.e. no allowance for children | ||||
|     :param widget_type: | ||||
|     :return: | ||||
|     """ | ||||
|     return ( | ||||
|         part_schema(widget_type) | ||||
|         .extend(FLAG_SCHEMA) | ||||
|         .extend(ALIGN_TO_SCHEMA) | ||||
|         .extend( | ||||
|             cv.Schema( | ||||
|                 { | ||||
|                     cv.Optional(CONF_STATE): SET_STATE_SCHEMA, | ||||
|                 } | ||||
|             ) | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|  | ||||
| ALIGN_TO_SCHEMA = { | ||||
|     cv.Optional(df.CONF_ALIGN_TO): cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.use_id(ty.lv_obj_t), | ||||
|             cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of, | ||||
|             cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent, | ||||
|             cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent, | ||||
|         } | ||||
|     ) | ||||
| } | ||||
|  | ||||
|  | ||||
| # A style schema that can include text | ||||
| STYLED_TEXT_SCHEMA = cv.maybe_simple_value( | ||||
|     STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT | ||||
| ) | ||||
|  | ||||
|  | ||||
| ALL_STYLES = { | ||||
|     **STYLE_PROPS, | ||||
| } | ||||
|  | ||||
|  | ||||
| def container_validator(schema, widget_type): | ||||
|     """ | ||||
|     Create a validator for a container given the widget type | ||||
|     :param schema: Base schema to extend | ||||
|     :param widget_type: | ||||
|     :return: | ||||
|     """ | ||||
|  | ||||
|     def validator(value): | ||||
|         result = schema | ||||
|         if w_sch := WIDGET_TYPES[widget_type].schema: | ||||
|             result = result.extend(w_sch) | ||||
|         if value and (layout := value.get(df.CONF_LAYOUT)): | ||||
|             if not isinstance(layout, dict): | ||||
|                 raise cv.Invalid("Layout value must be a dict") | ||||
|             ltype = layout.get(CONF_TYPE) | ||||
|             add_lv_use(ltype) | ||||
|         if value == SCHEMA_EXTRACT: | ||||
|             return result | ||||
|         return result(value) | ||||
|  | ||||
|     return validator | ||||
|  | ||||
|  | ||||
| def container_schema(widget_type, extras=None): | ||||
|     """ | ||||
|     Create a schema for a container widget of a given type. All obj properties are available, plus | ||||
|     the extras passed in, plus any defined for the specific widget being specified. | ||||
|     :param widget_type:     The widget type, e.g. "img" | ||||
|     :param extras:  Additional options to be made available, e.g. layout properties for children | ||||
|     :return: The schema for this type of widget. | ||||
|     """ | ||||
|     lv_type = get_widget_type(widget_type) | ||||
|     schema = obj_schema(widget_type).extend({cv.GenerateID(): cv.declare_id(lv_type)}) | ||||
|     if extras: | ||||
|         schema = schema.extend(extras) | ||||
|     # Delayed evaluation for recursion | ||||
|     return container_validator(schema, widget_type) | ||||
|  | ||||
|  | ||||
| def widget_schema(widget_type, extras=None): | ||||
|     """ | ||||
|     Create a schema for a given widget type | ||||
|     :param widget_type: The name of the widget | ||||
|     :param extras: | ||||
|     :return: | ||||
|     """ | ||||
|     validator = container_schema(widget_type, extras=extras) | ||||
|     if required := REQUIRED_COMPONENTS.get(widget_type): | ||||
|         validator = cv.All(validator, requires_component(required)) | ||||
|     return cv.Exclusive(widget_type, df.CONF_WIDGETS), validator | ||||
|  | ||||
|  | ||||
| # All widget schemas must be defined before this is called. | ||||
|  | ||||
|  | ||||
| def any_widget_schema(extras=None): | ||||
|     """ | ||||
|     Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of | ||||
|     widget under the widgets: key. | ||||
|  | ||||
|     :param extras: Additional schema to be applied to each generated one | ||||
|     :return: | ||||
|     """ | ||||
|     return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_PARTS)) | ||||
							
								
								
									
										64
									
								
								esphome/components/lvgl/types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								esphome/components/lvgl/types.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| from esphome import codegen as cg | ||||
| from esphome.core import ID | ||||
|  | ||||
| from .defines import CONF_LABEL, CONF_OBJ, CONF_TEXT | ||||
|  | ||||
| uint16_t_ptr = cg.uint16.operator("ptr") | ||||
| lvgl_ns = cg.esphome_ns.namespace("lvgl") | ||||
| char_ptr = cg.global_ns.namespace("char").operator("ptr") | ||||
| void_ptr = cg.void.operator("ptr") | ||||
| LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) | ||||
| lv_event_code_t = cg.global_ns.namespace("lv_event_code_t") | ||||
| FontEngine = lvgl_ns.class_("FontEngine") | ||||
| LvCompound = lvgl_ns.class_("LvCompound") | ||||
| lv_font_t = cg.global_ns.class_("lv_font_t") | ||||
| lv_style_t = cg.global_ns.struct("lv_style_t") | ||||
| lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton") | ||||
| lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) | ||||
| lv_obj_t_ptr = lv_obj_base_t.operator("ptr") | ||||
| lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr") | ||||
| lv_color_t = cg.global_ns.struct("lv_color_t") | ||||
|  | ||||
|  | ||||
| # this will be populated later, in __init__.py to avoid circular imports. | ||||
| WIDGET_TYPES: dict = {} | ||||
|  | ||||
|  | ||||
| class LvType(cg.MockObjClass): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         parens = kwargs.pop("parents", ()) | ||||
|         super().__init__(*args, parents=parens + (lv_obj_base_t,)) | ||||
|         self.args = kwargs.pop("largs", [(lv_obj_t_ptr, "obj")]) | ||||
|         self.value = kwargs.pop("lvalue", lambda w: w.obj) | ||||
|         self.has_on_value = kwargs.pop("has_on_value", False) | ||||
|         self.value_property = None | ||||
|  | ||||
|     def get_arg_type(self): | ||||
|         return self.args[0][0] if len(self.args) else None | ||||
|  | ||||
|  | ||||
| class LvText(LvType): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__( | ||||
|             *args, | ||||
|             largs=[(cg.std_string, "text")], | ||||
|             lvalue=lambda w: w.get_property("text")[0], | ||||
|             **kwargs, | ||||
|         ) | ||||
|         self.value_property = CONF_TEXT | ||||
|  | ||||
|  | ||||
| lv_obj_t = LvType("lv_obj_t") | ||||
| lv_label_t = LvText("lv_label_t") | ||||
|  | ||||
| LV_TYPES = { | ||||
|     CONF_LABEL: lv_label_t, | ||||
|     CONF_OBJ: lv_obj_t, | ||||
| } | ||||
|  | ||||
|  | ||||
| def get_widget_type(typestr: str) -> LvType: | ||||
|     return LV_TYPES[typestr] | ||||
|  | ||||
|  | ||||
| CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t) | ||||
							
								
								
									
										347
									
								
								esphome/components/lvgl/widget.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								esphome/components/lvgl/widget.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,347 @@ | ||||
| import sys | ||||
| from typing import Any | ||||
|  | ||||
| from esphome import codegen as cg, config_validation as cv | ||||
| from esphome.config_validation import Invalid | ||||
| from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE | ||||
| from esphome.core import ID, TimePeriod | ||||
| from esphome.coroutine import FakeAwaitable | ||||
| from esphome.cpp_generator import MockObjClass | ||||
|  | ||||
| from .defines import ( | ||||
|     CONF_DEFAULT, | ||||
|     CONF_MAIN, | ||||
|     CONF_SCROLLBAR_MODE, | ||||
|     CONF_WIDGETS, | ||||
|     OBJ_FLAGS, | ||||
|     PARTS, | ||||
|     STATES, | ||||
|     LValidator, | ||||
|     join_enums, | ||||
| ) | ||||
| from .helpers import add_lv_use | ||||
| from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj | ||||
| from .schemas import ALL_STYLES | ||||
| from .types import WIDGET_TYPES, LvCompound, lv_obj_t | ||||
|  | ||||
| EVENT_LAMB = "event_lamb__" | ||||
|  | ||||
|  | ||||
| class WidgetType: | ||||
|     """ | ||||
|     Describes a type of Widget, e.g. "bar" or "line" | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, name, schema=None, modify_schema=None): | ||||
|         """ | ||||
|         :param name: The widget name, e.g. "bar" | ||||
|         :param schema: The config schema for defining a widget | ||||
|         :param modify_schema: A schema to update the widget | ||||
|         """ | ||||
|         self.name = name | ||||
|         self.schema = schema or {} | ||||
|         if modify_schema is None: | ||||
|             self.modify_schema = schema | ||||
|         else: | ||||
|             self.modify_schema = modify_schema | ||||
|  | ||||
|     @property | ||||
|     def animated(self): | ||||
|         return False | ||||
|  | ||||
|     @property | ||||
|     def w_type(self): | ||||
|         """ | ||||
|         Get the type associated with this widget | ||||
|         :return: | ||||
|         """ | ||||
|         return lv_obj_t | ||||
|  | ||||
|     def is_compound(self): | ||||
|         return self.w_type.inherits_from(LvCompound) | ||||
|  | ||||
|     async def to_code(self, w, config: dict): | ||||
|         """ | ||||
|         Generate code for a given widget | ||||
|         :param w: The widget | ||||
|         :param config: Its configuration | ||||
|         :return: Generated code as a list of text lines | ||||
|         """ | ||||
|         raise NotImplementedError(f"No to_code defined for {self.name}") | ||||
|  | ||||
|     def obj_creator(self, parent: MockObjClass, config: dict): | ||||
|         """ | ||||
|         Create an instance of the widget type | ||||
|         :param parent: The parent to which it should be attached | ||||
|         :param config:  Its configuration | ||||
|         :return: Generated code as a single text line | ||||
|         """ | ||||
|         return f"lv_{self.name}_create({parent})" | ||||
|  | ||||
|     def get_uses(self): | ||||
|         """ | ||||
|         Get a list of other widgets used by this one | ||||
|         :return: | ||||
|         """ | ||||
|         return () | ||||
|  | ||||
|  | ||||
| class LvScrActType(WidgetType): | ||||
|     """ | ||||
|     A "widget" representing the active screen. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self): | ||||
|         super().__init__("lv_scr_act()") | ||||
|  | ||||
|     def obj_creator(self, parent: MockObjClass, config: dict): | ||||
|         return [] | ||||
|  | ||||
|     async def to_code(self, w, config: dict): | ||||
|         return [] | ||||
|  | ||||
|  | ||||
| class Widget: | ||||
|     """ | ||||
|     Represents a Widget. | ||||
|     """ | ||||
|  | ||||
|     widgets_completed = False | ||||
|  | ||||
|     @staticmethod | ||||
|     def set_completed(): | ||||
|         Widget.widgets_completed = True | ||||
|  | ||||
|     def __init__(self, var, wtype: WidgetType, config: dict = None, parent=None): | ||||
|         self.var = var | ||||
|         self.type = wtype | ||||
|         self.config = config | ||||
|         self.scale = 1.0 | ||||
|         self.step = 1.0 | ||||
|         self.range_from = -sys.maxsize | ||||
|         self.range_to = sys.maxsize | ||||
|         self.parent = parent | ||||
|  | ||||
|     @staticmethod | ||||
|     def create(name, var, wtype: WidgetType, config: dict = None, parent=None): | ||||
|         w = Widget(var, wtype, config, parent) | ||||
|         if name is not None: | ||||
|             widget_map[name] = w | ||||
|         return w | ||||
|  | ||||
|     @property | ||||
|     def obj(self): | ||||
|         if self.type.is_compound(): | ||||
|             return f"{self.var}->obj" | ||||
|         return self.var | ||||
|  | ||||
|     def add_state(self, *args): | ||||
|         return lv_obj.add_state(self.obj, *args) | ||||
|  | ||||
|     def clear_state(self, *args): | ||||
|         return lv_obj.clear_state(self.obj, *args) | ||||
|  | ||||
|     def add_flag(self, *args): | ||||
|         return lv_obj.add_flag(self.obj, *args) | ||||
|  | ||||
|     def clear_flag(self, *args): | ||||
|         return lv_obj.clear_flag(self.obj, *args) | ||||
|  | ||||
|     def set_property(self, prop, value, animated: bool = None, ltype=None): | ||||
|         if isinstance(value, dict): | ||||
|             value = value.get(prop) | ||||
|         if value is None: | ||||
|             return | ||||
|         if isinstance(value, TimePeriod): | ||||
|             value = value.total_milliseconds | ||||
|         ltype = ltype or self.__type_base() | ||||
|         if animated is None or self.type.animated is not True: | ||||
|             lv.call(f"{ltype}_set_{prop}", self.obj, value) | ||||
|         else: | ||||
|             lv.call( | ||||
|                 f"{ltype}_set_{prop}", | ||||
|                 self.obj, | ||||
|                 value, | ||||
|                 "LV_ANIM_ON" if animated else "LV_ANIM_OFF", | ||||
|             ) | ||||
|  | ||||
|     def get_property(self, prop, ltype=None): | ||||
|         ltype = ltype or self.__type_base() | ||||
|         return f"lv_{ltype}_get_{prop}({self.obj})" | ||||
|  | ||||
|     def set_style(self, prop, value, state): | ||||
|         if value is None: | ||||
|             return [] | ||||
|         return lv.call(f"obj_set_style_{prop}", self.obj, value, state) | ||||
|  | ||||
|     def __type_base(self): | ||||
|         wtype = self.type.w_type | ||||
|         base = str(wtype) | ||||
|         if base.startswith("Lv"): | ||||
|             return f"{wtype}".removeprefix("Lv").removesuffix("Type").lower() | ||||
|         return f"{wtype}".removeprefix("lv_").removesuffix("_t") | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f"({self.var}, {self.type})" | ||||
|  | ||||
|  | ||||
| # Map of widgets to their config, used for trigger generation | ||||
| widget_map: dict[Any, Widget] = {} | ||||
|  | ||||
|  | ||||
| def get_widget_generator(wid): | ||||
|     """ | ||||
|     Used to wait for a widget during code generation. | ||||
|     :param wid: | ||||
|     :return: | ||||
|     """ | ||||
|     while True: | ||||
|         if obj := widget_map.get(wid): | ||||
|             return obj | ||||
|         if Widget.widgets_completed: | ||||
|             raise Invalid( | ||||
|                 f"Widget {wid} not found, yet all widgets should be defined by now" | ||||
|             ) | ||||
|         yield | ||||
|  | ||||
|  | ||||
| async def get_widget(wid: ID) -> Widget: | ||||
|     if obj := widget_map.get(wid): | ||||
|         return obj | ||||
|     return await FakeAwaitable(get_widget_generator(wid)) | ||||
|  | ||||
|  | ||||
| def collect_props(config): | ||||
|     """ | ||||
|     Collect all properties from a configuration | ||||
|     :param config: | ||||
|     :return: | ||||
|     """ | ||||
|     props = {} | ||||
|     for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_GROUP]: | ||||
|         if prop in config: | ||||
|             props[prop] = config[prop] | ||||
|     return props | ||||
|  | ||||
|  | ||||
| def collect_states(config): | ||||
|     """ | ||||
|     Collect prperties for each state of a widget | ||||
|     :param config: | ||||
|     :return: | ||||
|     """ | ||||
|     states = {CONF_DEFAULT: collect_props(config)} | ||||
|     for state in STATES: | ||||
|         if state in config: | ||||
|             states[state] = collect_props(config[state]) | ||||
|     return states | ||||
|  | ||||
|  | ||||
| def collect_parts(config): | ||||
|     """ | ||||
|     Collect properties and states for all widget parts | ||||
|     :param config: | ||||
|     :return: | ||||
|     """ | ||||
|     parts = {CONF_MAIN: collect_states(config)} | ||||
|     for part in PARTS: | ||||
|         if part in config: | ||||
|             parts[part] = collect_states(config[part]) | ||||
|     return parts | ||||
|  | ||||
|  | ||||
| async def set_obj_properties(w: Widget, config): | ||||
|     """Generate a list of C++ statements to apply properties to an lv_obj_t""" | ||||
|     parts = collect_parts(config) | ||||
|     for part, states in parts.items(): | ||||
|         for state, props in states.items(): | ||||
|             lv_state = ConstantLiteral( | ||||
|                 f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}" | ||||
|             ) | ||||
|             for prop, value in { | ||||
|                 k: v for k, v in props.items() if k in ALL_STYLES | ||||
|             }.items(): | ||||
|                 if isinstance(ALL_STYLES[prop], LValidator): | ||||
|                     value = await ALL_STYLES[prop].process(value) | ||||
|                 w.set_style(prop, value, lv_state) | ||||
|     flag_clr = set() | ||||
|     flag_set = set() | ||||
|     props = parts[CONF_MAIN][CONF_DEFAULT] | ||||
|     for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): | ||||
|         if value: | ||||
|             flag_set.add(prop) | ||||
|         else: | ||||
|             flag_clr.add(prop) | ||||
|     if flag_set: | ||||
|         adds = join_enums(flag_set, "LV_OBJ_FLAG_") | ||||
|         w.add_flag(adds) | ||||
|     if flag_clr: | ||||
|         clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") | ||||
|         w.clear_flag(clrs) | ||||
|  | ||||
|     if states := config.get(CONF_STATE): | ||||
|         adds = set() | ||||
|         clears = set() | ||||
|         lambs = {} | ||||
|         for key, value in states.items(): | ||||
|             if isinstance(value, cv.Lambda): | ||||
|                 lambs[key] = value | ||||
|             elif value == "true": | ||||
|                 adds.add(key) | ||||
|             else: | ||||
|                 clears.add(key) | ||||
|         if adds: | ||||
|             adds = ConstantLiteral(join_enums(adds, "LV_STATE_")) | ||||
|             w.add_state(adds) | ||||
|         if clears: | ||||
|             clears = ConstantLiteral(join_enums(clears, "LV_STATE_")) | ||||
|             w.clear_state(clears) | ||||
|         for key, value in lambs.items(): | ||||
|             lamb = await cg.process_lambda(value, [], return_type=cg.bool_) | ||||
|             state = ConstantLiteral(f"LV_STATE_{key.upper}") | ||||
|             lv.cond_if(lamb) | ||||
|             w.add_state(state) | ||||
|             lv.cond_else() | ||||
|             w.clear_state(state) | ||||
|             lv.cond_endif() | ||||
|     if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE): | ||||
|         lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode) | ||||
|  | ||||
|  | ||||
| async def add_widgets(parent: Widget, config: dict): | ||||
|     """ | ||||
|     Add all widgets to an object | ||||
|     :param parent: The enclosing obj | ||||
|     :param config: The configuration | ||||
|     :return: | ||||
|     """ | ||||
|     for w in config.get(CONF_WIDGETS) or (): | ||||
|         w_type, w_cnfig = next(iter(w.items())) | ||||
|         await widget_to_code(w_cnfig, w_type, parent.obj) | ||||
|  | ||||
|  | ||||
| async def widget_to_code(w_cnfig, w_type, parent): | ||||
|     """ | ||||
|     Converts a Widget definition to C code. | ||||
|     :param w_cnfig: The widget configuration | ||||
|     :param w_type:  The Widget type | ||||
|     :param parent: The parent to which the widget should be added | ||||
|     :return: | ||||
|     """ | ||||
|     spec: WidgetType = WIDGET_TYPES[w_type] | ||||
|     creator = spec.obj_creator(parent, w_cnfig) | ||||
|     add_lv_use(spec.name) | ||||
|     add_lv_use(*spec.get_uses()) | ||||
|     wid = w_cnfig[CONF_ID] | ||||
|     add_line_marks(wid) | ||||
|     if spec.is_compound(): | ||||
|         var = cg.new_Pvariable(wid) | ||||
|         lv_add(var.set_obj(creator)) | ||||
|     else: | ||||
|         var = cg.Pvariable(wid, cg.nullptr, type_=lv_obj_t) | ||||
|         lv_assign(var, creator) | ||||
|  | ||||
|     widget = Widget.create(wid, var, spec, w_cnfig, parent) | ||||
|     await set_obj_properties(widget, w_cnfig) | ||||
|     await add_widgets(widget, w_cnfig) | ||||
|     await spec.to_code(widget, w_cnfig) | ||||
							
								
								
									
										33
									
								
								esphome/components/m5stack_8angle/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/m5stack_8angle/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
| CODEOWNERS = ["@rnauber"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| CONF_M5STACK_8ANGLE_ID = "m5stack_8angle_id" | ||||
|  | ||||
| m5stack_8angle_ns = cg.esphome_ns.namespace("m5stack_8angle") | ||||
| M5Stack8AngleComponent = m5stack_8angle_ns.class_( | ||||
|     "M5Stack8AngleComponent", | ||||
|     i2c.I2CDevice, | ||||
|     cg.Component, | ||||
| ) | ||||
|  | ||||
| AnalogBits = m5stack_8angle_ns.enum("AnalogBits") | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(M5Stack8AngleComponent), | ||||
|     } | ||||
| ).extend(i2c.i2c_device_schema(0x43)) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
							
								
								
									
										30
									
								
								esphome/components/m5stack_8angle/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/m5stack_8angle/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor | ||||
|  | ||||
| from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID | ||||
|  | ||||
|  | ||||
| M5Stack8AngleSwitchBinarySensor = m5stack_8angle_ns.class_( | ||||
|     "M5Stack8AngleSwitchBinarySensor", | ||||
|     binary_sensor.BinarySensor, | ||||
|     cg.PollingComponent, | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent), | ||||
|         } | ||||
|     ) | ||||
|     .extend(binary_sensor.binary_sensor_schema(M5Stack8AngleSwitchBinarySensor)) | ||||
|     .extend(cv.polling_component_schema("10s")) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID]) | ||||
|     sens = await binary_sensor.new_binary_sensor(config) | ||||
|     cg.add(sens.set_parent(hub)) | ||||
|     await cg.register_component(sens, config) | ||||
| @@ -0,0 +1,17 @@ | ||||
| #include "m5stack_8angle_binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace m5stack_8angle { | ||||
|  | ||||
| void M5Stack8AngleSwitchBinarySensor::update() { | ||||
|   int8_t out = this->parent_->read_switch(); | ||||
|   if (out == -1) { | ||||
|     this->status_set_warning("Could not read binary sensor state from M5Stack 8Angle."); | ||||
|     return; | ||||
|   } | ||||
|   this->publish_state(out != 0); | ||||
|   this->status_clear_warning(); | ||||
| } | ||||
|  | ||||
| }  // namespace m5stack_8angle | ||||
| }  // namespace esphome | ||||
| @@ -0,0 +1,19 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #include "../m5stack_8angle.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace m5stack_8angle { | ||||
|  | ||||
| class M5Stack8AngleSwitchBinarySensor : public binary_sensor::BinarySensor, | ||||
|                                         public PollingComponent, | ||||
|                                         public Parented<M5Stack8AngleComponent> { | ||||
|  public: | ||||
|   void update() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace m5stack_8angle | ||||
| }  // namespace esphome | ||||
							
								
								
									
										31
									
								
								esphome/components/m5stack_8angle/light/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								esphome/components/m5stack_8angle/light/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import light | ||||
|  | ||||
| from esphome.const import CONF_OUTPUT_ID | ||||
|  | ||||
| from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID | ||||
|  | ||||
|  | ||||
| M5Stack8AngleLightsComponent = m5stack_8angle_ns.class_( | ||||
|     "M5Stack8AngleLightOutput", | ||||
|     light.AddressableLight, | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     light.ADDRESSABLE_LIGHT_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent), | ||||
|             cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(M5Stack8AngleLightsComponent), | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID]) | ||||
|     lights = cg.new_Pvariable(config[CONF_OUTPUT_ID]) | ||||
|     await light.register_light(lights, config) | ||||
|     await cg.register_component(lights, config) | ||||
|     cg.add(lights.set_parent(hub)) | ||||
| @@ -0,0 +1,45 @@ | ||||
| #include "m5stack_8angle_light.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace m5stack_8angle { | ||||
|  | ||||
| static const char *const TAG = "m5stack_8angle.light"; | ||||
|  | ||||
| void M5Stack8AngleLightOutput::setup() { | ||||
|   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|   this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); | ||||
|   if (this->buf_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   }; | ||||
|   memset(this->buf_, 0xFF, M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); | ||||
|  | ||||
|   this->effect_data_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS); | ||||
|   if (this->effect_data_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Failed to allocate effect data of size %u", M5STACK_8ANGLE_NUM_LEDS); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   }; | ||||
|   memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS); | ||||
| } | ||||
|  | ||||
| void M5Stack8AngleLightOutput::write_state(light::LightState *state) { | ||||
|   for (int i = 0; i < M5STACK_8ANGLE_NUM_LEDS; | ||||
|        i++) {  // write one LED at a time, otherwise the message will be truncated | ||||
|     this->parent_->write_register(M5STACK_8ANGLE_REGISTER_RGB_24B + i * M5STACK_8ANGLE_BYTES_PER_LED, | ||||
|                                   this->buf_ + i * M5STACK_8ANGLE_BYTES_PER_LED, M5STACK_8ANGLE_BYTES_PER_LED); | ||||
|   } | ||||
| } | ||||
|  | ||||
| light::ESPColorView M5Stack8AngleLightOutput::get_view_internal(int32_t index) const { | ||||
|   size_t pos = index * M5STACK_8ANGLE_BYTES_PER_LED; | ||||
|   // red, green, blue, white, effect_data, color_correction | ||||
|   return {this->buf_ + pos, this->buf_ + pos + 1,       this->buf_ + pos + 2, | ||||
|           nullptr,          this->effect_data_ + index, &this->correction_}; | ||||
| } | ||||
|  | ||||
| }  // namespace m5stack_8angle | ||||
| }  // namespace esphome | ||||
| @@ -0,0 +1,37 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/light/addressable_light.h" | ||||
| #include "esphome/components/light/light_output.h" | ||||
|  | ||||
| #include "../m5stack_8angle.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace m5stack_8angle { | ||||
|  | ||||
| static const uint8_t M5STACK_8ANGLE_NUM_LEDS = 9; | ||||
| static const uint8_t M5STACK_8ANGLE_BYTES_PER_LED = 4; | ||||
|  | ||||
| class M5Stack8AngleLightOutput : public light::AddressableLight, public Parented<M5Stack8AngleComponent> { | ||||
|  public: | ||||
|   void setup() override; | ||||
|  | ||||
|   void write_state(light::LightState *state) override; | ||||
|  | ||||
|   int32_t size() const override { return M5STACK_8ANGLE_NUM_LEDS; } | ||||
|   light::LightTraits get_traits() override { | ||||
|     auto traits = light::LightTraits(); | ||||
|     traits.set_supported_color_modes({light::ColorMode::RGB}); | ||||
|     return traits; | ||||
|   }; | ||||
|  | ||||
|   void clear_effect_data() override { memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS); }; | ||||
|  | ||||
|  protected: | ||||
|   light::ESPColorView get_view_internal(int32_t index) const override; | ||||
|  | ||||
|   uint8_t *buf_{nullptr}; | ||||
|   uint8_t *effect_data_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace m5stack_8angle | ||||
| }  // namespace esphome | ||||
							
								
								
									
										74
									
								
								esphome/components/m5stack_8angle/m5stack_8angle.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								esphome/components/m5stack_8angle/m5stack_8angle.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| #include "m5stack_8angle.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace m5stack_8angle { | ||||
|  | ||||
| static const char *const TAG = "m5stack_8angle"; | ||||
|  | ||||
| void M5Stack8AngleComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up M5STACK_8ANGLE..."); | ||||
|   i2c::ErrorCode err; | ||||
|  | ||||
|   err = this->read(nullptr, 0); | ||||
|   if (err != i2c::NO_ERROR) { | ||||
|     ESP_LOGE(TAG, "I2C error %02X...", err); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   }; | ||||
|  | ||||
|   err = this->read_register(M5STACK_8ANGLE_REGISTER_FW_VERSION, &this->fw_version_, 1); | ||||
|   if (err != i2c::NO_ERROR) { | ||||
|     ESP_LOGE(TAG, "I2C error %02X...", err); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| void M5Stack8AngleComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "M5STACK_8ANGLE:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   ESP_LOGCONFIG(TAG, "  Firmware version: %d ", this->fw_version_); | ||||
| } | ||||
|  | ||||
| float M5Stack8AngleComponent::read_knob_pos(uint8_t channel, AnalogBits bits) { | ||||
|   int32_t raw_pos = this->read_knob_pos_raw(channel, bits); | ||||
|   if (raw_pos == -1) { | ||||
|     return NAN; | ||||
|   } | ||||
|   return (float) raw_pos / ((1 << bits) - 1); | ||||
| } | ||||
|  | ||||
| int32_t M5Stack8AngleComponent::read_knob_pos_raw(uint8_t channel, AnalogBits bits) { | ||||
|   uint16_t knob_pos = 0; | ||||
|   i2c::ErrorCode err; | ||||
|   if (bits == BITS_8) { | ||||
|     err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B + channel, (uint8_t *) &knob_pos, 1); | ||||
|   } else if (bits == BITS_12) { | ||||
|     err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B + (channel * 2), (uint8_t *) &knob_pos, 2); | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "Invalid number of bits: %d", bits); | ||||
|     return -1; | ||||
|   } | ||||
|   if (err == i2c::NO_ERROR) { | ||||
|     return knob_pos; | ||||
|   } else { | ||||
|     return -1; | ||||
|   } | ||||
| } | ||||
|  | ||||
| int8_t M5Stack8AngleComponent::read_switch() { | ||||
|   uint8_t out; | ||||
|   i2c::ErrorCode err = this->read_register(M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT, (uint8_t *) &out, 1); | ||||
|   if (err == i2c::NO_ERROR) { | ||||
|     return out ? 1 : 0; | ||||
|   } else { | ||||
|     return -1; | ||||
|   } | ||||
| } | ||||
|  | ||||
| float M5Stack8AngleComponent::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| }  // namespace m5stack_8angle | ||||
| }  // namespace esphome | ||||
							
								
								
									
										34
									
								
								esphome/components/m5stack_8angle/m5stack_8angle.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/m5stack_8angle/m5stack_8angle.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace m5stack_8angle { | ||||
|  | ||||
| static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B = 0x00; | ||||
| static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B = 0x10; | ||||
| static const uint8_t M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT = 0x20; | ||||
| static const uint8_t M5STACK_8ANGLE_REGISTER_RGB_24B = 0x30; | ||||
| static const uint8_t M5STACK_8ANGLE_REGISTER_FW_VERSION = 0xFE; | ||||
|  | ||||
| enum AnalogBits : uint8_t { | ||||
|   BITS_8 = 8, | ||||
|   BITS_12 = 12, | ||||
| }; | ||||
|  | ||||
| class M5Stack8AngleComponent : public i2c::I2CDevice, public Component { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   float read_knob_pos(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); | ||||
|   int32_t read_knob_pos_raw(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); | ||||
|   int8_t read_switch(); | ||||
|  | ||||
|  protected: | ||||
|   uint8_t fw_version_; | ||||
| }; | ||||
|  | ||||
| }  // namespace m5stack_8angle | ||||
| }  // namespace esphome | ||||
							
								
								
									
										66
									
								
								esphome/components/m5stack_8angle/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								esphome/components/m5stack_8angle/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor | ||||
|  | ||||
| from esphome.const import ( | ||||
|     CONF_BIT_DEPTH, | ||||
|     CONF_CHANNEL, | ||||
|     CONF_RAW, | ||||
|     ICON_ROTATE_RIGHT, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
| ) | ||||
|  | ||||
| from .. import ( | ||||
|     AnalogBits, | ||||
|     M5Stack8AngleComponent, | ||||
|     m5stack_8angle_ns, | ||||
|     CONF_M5STACK_8ANGLE_ID, | ||||
| ) | ||||
|  | ||||
|  | ||||
| M5Stack8AngleKnobSensor = m5stack_8angle_ns.class_( | ||||
|     "M5Stack8AngleKnobSensor", | ||||
|     sensor.Sensor, | ||||
|     cg.PollingComponent, | ||||
| ) | ||||
|  | ||||
|  | ||||
| BIT_DEPTHS = { | ||||
|     8: AnalogBits.BITS_8, | ||||
|     12: AnalogBits.BITS_12, | ||||
| } | ||||
|  | ||||
| _validate_bits = cv.float_with_unit("bits", "bit") | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(M5Stack8AngleKnobSensor), | ||||
|             cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent), | ||||
|             cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=8), | ||||
|             cv.Optional(CONF_BIT_DEPTH, default="8bit"): cv.All( | ||||
|                 _validate_bits, cv.enum(BIT_DEPTHS) | ||||
|             ), | ||||
|             cv.Optional(CONF_RAW, default=False): cv.boolean, | ||||
|         } | ||||
|     ) | ||||
|     .extend( | ||||
|         sensor.sensor_schema( | ||||
|             M5Stack8AngleKnobSensor, | ||||
|             accuracy_decimals=2, | ||||
|             icon=ICON_ROTATE_RIGHT, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ) | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("10s")) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await cg.register_parented(var, config[CONF_M5STACK_8ANGLE_ID]) | ||||
|     cg.add(var.set_channel(config[CONF_CHANNEL] - 1)) | ||||
|     cg.add(var.set_bit_depth(BIT_DEPTHS[config[CONF_BIT_DEPTH]])) | ||||
|     cg.add(var.set_raw(config[CONF_RAW])) | ||||
| @@ -0,0 +1,24 @@ | ||||
| #include "m5stack_8angle_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace m5stack_8angle { | ||||
|  | ||||
| void M5Stack8AngleKnobSensor::update() { | ||||
|   if (this->parent_ != nullptr) { | ||||
|     int32_t raw_pos = this->parent_->read_knob_pos_raw(this->channel_, this->bits_); | ||||
|     if (raw_pos == -1) { | ||||
|       this->status_set_warning("Could not read knob position from M5Stack 8Angle."); | ||||
|       return; | ||||
|     } | ||||
|     if (this->raw_) { | ||||
|       this->publish_state(raw_pos); | ||||
|     } else { | ||||
|       float knob_pos = (float) raw_pos / ((1 << this->bits_) - 1); | ||||
|       this->publish_state(knob_pos); | ||||
|     } | ||||
|     this->status_clear_warning(); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| }  // namespace m5stack_8angle | ||||
| }  // namespace esphome | ||||
| @@ -0,0 +1,27 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #include "../m5stack_8angle.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace m5stack_8angle { | ||||
|  | ||||
| class M5Stack8AngleKnobSensor : public sensor::Sensor, | ||||
|                                 public PollingComponent, | ||||
|                                 public Parented<M5Stack8AngleComponent> { | ||||
|  public: | ||||
|   void update() override; | ||||
|   void set_channel(uint8_t channel) { this->channel_ = channel; }; | ||||
|   void set_bit_depth(AnalogBits bits) { this->bits_ = bits; }; | ||||
|   void set_raw(bool raw) { this->raw_ = raw; }; | ||||
|  | ||||
|  protected: | ||||
|   uint8_t channel_; | ||||
|   AnalogBits bits_; | ||||
|   bool raw_; | ||||
| }; | ||||
|  | ||||
| }  // namespace m5stack_8angle | ||||
| }  // namespace esphome | ||||
| @@ -357,7 +357,9 @@ CONFIG_SCHEMA = cv.All( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(MicroWakeWord), | ||||
|             cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), | ||||
|             cv.Required(CONF_MODELS): cv.ensure_list(MODEL_SCHEMA), | ||||
|             cv.Required(CONF_MODELS): cv.ensure_list( | ||||
|                 cv.maybe_simple_value(MODEL_SCHEMA, key=CONF_MODEL) | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_WAKE_WORD_DETECTED): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
|   | ||||
| @@ -1,8 +1,16 @@ | ||||
| import binascii | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.components import modbus | ||||
| from esphome.const import CONF_ADDRESS, CONF_ID, CONF_NAME, CONF_LAMBDA, CONF_OFFSET | ||||
| from esphome.const import ( | ||||
|     CONF_ADDRESS, | ||||
|     CONF_ID, | ||||
|     CONF_NAME, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_OFFSET, | ||||
|     CONF_TRIGGER_ID, | ||||
| ) | ||||
| from esphome.cpp_helpers import logging | ||||
| from .const import ( | ||||
|     CONF_BITMASK, | ||||
| @@ -12,6 +20,7 @@ from .const import ( | ||||
|     CONF_CUSTOM_COMMAND, | ||||
|     CONF_FORCE_NEW_RANGE, | ||||
|     CONF_MODBUS_CONTROLLER_ID, | ||||
|     CONF_ON_COMMAND_SENT, | ||||
|     CONF_REGISTER_COUNT, | ||||
|     CONF_REGISTER_TYPE, | ||||
|     CONF_RESPONSE_SIZE, | ||||
| @@ -97,6 +106,10 @@ TYPE_REGISTER_MAP = { | ||||
|     "FP32_R": 2, | ||||
| } | ||||
|  | ||||
| ModbusCommandSentTrigger = modbus_controller_ns.class_( | ||||
|     "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) | ||||
| ) | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| ModbusServerRegisterSchema = cv.Schema( | ||||
| @@ -120,13 +133,19 @@ CONFIG_SCHEMA = cv.All( | ||||
|             cv.Optional( | ||||
|                 CONF_SERVER_REGISTERS, | ||||
|             ): cv.ensure_list(ModbusServerRegisterSchema), | ||||
|             cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                         ModbusCommandSentTrigger | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(modbus.modbus_device_schema(0x01)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| ModbusItemBaseSchema = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), | ||||
| @@ -254,6 +273,11 @@ async def to_code(config): | ||||
|                 ) | ||||
|             ) | ||||
|     await register_modbus_device(var, config) | ||||
|     for conf in config.get(CONF_ON_COMMAND_SENT, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation( | ||||
|             trigger, [(int, "function_code"), (int, "address")], conf | ||||
|         ) | ||||
|  | ||||
|  | ||||
| async def register_modbus_device(var, config): | ||||
|   | ||||
							
								
								
									
										19
									
								
								esphome/components/modbus_controller/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/modbus_controller/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/components/modbus_controller/modbus_controller.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace modbus_controller { | ||||
|  | ||||
| class ModbusCommandSentTrigger : public Trigger<int, int> { | ||||
|  public: | ||||
|   ModbusCommandSentTrigger(ModbusController *a_modbuscontroller) { | ||||
|     a_modbuscontroller->add_on_command_sent_callback( | ||||
|         [this](int function_code, int address) { this->trigger(function_code, address); }); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| }  // namespace modbus_controller | ||||
| }  // namespace esphome | ||||
| @@ -6,6 +6,7 @@ CONF_CUSTOM_COMMAND = "custom_command" | ||||
| CONF_FORCE_NEW_RANGE = "force_new_range" | ||||
| CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id" | ||||
| CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode" | ||||
| CONF_ON_COMMAND_SENT = "on_command_sent" | ||||
| CONF_RAW_ENCODE = "raw_encode" | ||||
| CONF_REGISTER_COUNT = "register_count" | ||||
| CONF_REGISTER_TYPE = "register_type" | ||||
|   | ||||
| @@ -43,7 +43,11 @@ bool ModbusController::send_next_command_() { | ||||
|       ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_, | ||||
|                command->register_address, command->register_count); | ||||
|       command->send(); | ||||
|  | ||||
|       this->last_command_timestamp_ = millis(); | ||||
|  | ||||
|       this->command_sent_callback_.call((int) command->function_code, command->register_address); | ||||
|  | ||||
|       // remove from queue if no handler is defined | ||||
|       if (!command->on_data_func) { | ||||
|         command_queue_.pop_front(); | ||||
| @@ -659,5 +663,9 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens | ||||
|   return value; | ||||
| } | ||||
|  | ||||
| void ModbusController::add_on_command_sent_callback(std::function<void(int, int)> &&callback) { | ||||
|   this->command_sent_callback_.add(std::move(callback)); | ||||
| } | ||||
|  | ||||
| }  // namespace modbus_controller | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -456,6 +456,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { | ||||
|   size_t get_command_queue_length() { return command_queue_.size(); } | ||||
|   /// get if the module is offline, didn't respond the last command | ||||
|   bool get_module_offline() { return module_offline_; } | ||||
|   /// Set callback for commands | ||||
|   void add_on_command_sent_callback(std::function<void(int, int)> &&callback); | ||||
|  | ||||
|  protected: | ||||
|   /// parse sensormap_ and create range of sequential addresses | ||||
| @@ -488,6 +490,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { | ||||
|   bool module_offline_; | ||||
|   /// how many updates to skip if module is offline | ||||
|   uint16_t offline_skip_updates_; | ||||
|   CallbackManager<void(int, int)> command_sent_callback_{}; | ||||
| }; | ||||
|  | ||||
| /** Convert vector<uint8_t> response payload to float. | ||||
|   | ||||
| @@ -61,6 +61,7 @@ def AUTO_LOAD(): | ||||
|     return ["json"] | ||||
|  | ||||
|  | ||||
| CONF_DISCOVER_IP = "discover_ip" | ||||
| CONF_IDF_SEND_ASYNC = "idf_send_async" | ||||
| CONF_SKIP_CERT_CN_CHECK = "skip_cert_cn_check" | ||||
|  | ||||
| @@ -225,6 +226,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 cv.boolean, cv.one_of("CLEAN", upper=True) | ||||
|             ), | ||||
|             cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean, | ||||
|             cv.Optional(CONF_DISCOVER_IP, default=True): cv.boolean, | ||||
|             cv.Optional( | ||||
|                 CONF_DISCOVERY_PREFIX, default="homeassistant" | ||||
|             ): cv.publish_topic, | ||||
| @@ -328,8 +330,12 @@ async def to_code(config): | ||||
|     discovery_prefix = config[CONF_DISCOVERY_PREFIX] | ||||
|     discovery_unique_id_generator = config[CONF_DISCOVERY_UNIQUE_ID_GENERATOR] | ||||
|     discovery_object_id_generator = config[CONF_DISCOVERY_OBJECT_ID_GENERATOR] | ||||
|     discover_ip = config[CONF_DISCOVER_IP] | ||||
|  | ||||
|     if not discovery: | ||||
|         discovery_prefix = "" | ||||
|  | ||||
|     if not discovery and not discover_ip: | ||||
|         cg.add(var.disable_discovery()) | ||||
|     elif discovery == "CLEAN": | ||||
|         cg.add( | ||||
| @@ -338,6 +344,7 @@ async def to_code(config): | ||||
|                 discovery_unique_id_generator, | ||||
|                 discovery_object_id_generator, | ||||
|                 discovery_retain, | ||||
|                 discover_ip, | ||||
|                 True, | ||||
|             ) | ||||
|         ) | ||||
| @@ -348,6 +355,7 @@ async def to_code(config): | ||||
|                 discovery_unique_id_generator, | ||||
|                 discovery_object_id_generator, | ||||
|                 discovery_retain, | ||||
|                 discover_ip, | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -66,7 +66,7 @@ void MQTTClientComponent::setup() { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   if (this->is_discovery_enabled()) { | ||||
|   if (this->is_discovery_ip_enabled()) { | ||||
|     this->subscribe( | ||||
|         "esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, | ||||
|         2); | ||||
| @@ -82,7 +82,7 @@ void MQTTClientComponent::setup() { | ||||
| } | ||||
|  | ||||
| void MQTTClientComponent::send_device_info_() { | ||||
|   if (!this->is_connected() or !this->is_discovery_enabled()) { | ||||
|   if (!this->is_connected() or !this->is_discovery_ip_enabled()) { | ||||
|     return; | ||||
|   } | ||||
|   std::string topic = "esphome/discover/"; | ||||
| @@ -99,6 +99,9 @@ void MQTTClientComponent::send_device_info_() { | ||||
|           } | ||||
|         } | ||||
|         root["name"] = App.get_name(); | ||||
|         if (!App.get_friendly_name().empty()) { | ||||
|           root["friendly_name"] = App.get_friendly_name(); | ||||
|         } | ||||
| #ifdef USE_API | ||||
|         root["port"] = api::global_api_server->get_port(); | ||||
| #endif | ||||
| @@ -130,6 +133,10 @@ void MQTTClientComponent::send_device_info_() { | ||||
| #ifdef USE_DASHBOARD_IMPORT | ||||
|         root["package_import_url"] = dashboard_import::get_package_import_url(); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
|         root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; | ||||
| #endif | ||||
|       }, | ||||
|       2, this->discovery_info_.retain); | ||||
| } | ||||
| @@ -140,6 +147,9 @@ void MQTTClientComponent::dump_config() { | ||||
|                 this->ip_.str().c_str()); | ||||
|   ESP_LOGCONFIG(TAG, "  Username: " LOG_SECRET("'%s'"), this->credentials_.username.c_str()); | ||||
|   ESP_LOGCONFIG(TAG, "  Client ID: " LOG_SECRET("'%s'"), this->credentials_.client_id.c_str()); | ||||
|   if (this->is_discovery_ip_enabled()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Discovery IP enabled"); | ||||
|   } | ||||
|   if (!this->discovery_info_.prefix.empty()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Discovery prefix: '%s'", this->discovery_info_.prefix.c_str()); | ||||
|     ESP_LOGCONFIG(TAG, "  Discovery retain: %s", YESNO(this->discovery_info_.retain)); | ||||
| @@ -581,6 +591,7 @@ void MQTTClientComponent::disable_shutdown_message() { | ||||
|   this->recalculate_availability_(); | ||||
| } | ||||
| bool MQTTClientComponent::is_discovery_enabled() const { return !this->discovery_info_.prefix.empty(); } | ||||
| bool MQTTClientComponent::is_discovery_ip_enabled() const { return this->discovery_info_.discover_ip; } | ||||
| const Availability &MQTTClientComponent::get_availability() { return this->availability_; } | ||||
| void MQTTClientComponent::recalculate_availability_() { | ||||
|   if (this->birth_message_.topic.empty() || this->birth_message_.topic != this->last_will_.topic) { | ||||
| @@ -606,8 +617,9 @@ void MQTTClientComponent::set_shutdown_message(MQTTMessage &&message) { this->sh | ||||
|  | ||||
| void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, | ||||
|                                              MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, | ||||
|                                              bool clean) { | ||||
|                                              bool discover_ip, bool clean) { | ||||
|   this->discovery_info_.prefix = std::move(prefix); | ||||
|   this->discovery_info_.discover_ip = discover_ip; | ||||
|   this->discovery_info_.unique_id_generator = unique_id_generator; | ||||
|   this->discovery_info_.object_id_generator = object_id_generator; | ||||
|   this->discovery_info_.retain = retain; | ||||
|   | ||||
| @@ -79,6 +79,7 @@ enum MQTTDiscoveryObjectIdGenerator { | ||||
| struct MQTTDiscoveryInfo { | ||||
|   std::string prefix;  ///< The Home Assistant discovery prefix. Empty means disabled. | ||||
|   bool retain;         ///< Whether to retain discovery messages. | ||||
|   bool discover_ip;    ///< Enable the Home Assistant device discovery. | ||||
|   bool clean; | ||||
|   MQTTDiscoveryUniqueIdGenerator unique_id_generator; | ||||
|   MQTTDiscoveryObjectIdGenerator object_id_generator; | ||||
| @@ -122,12 +123,14 @@ class MQTTClientComponent : public Component { | ||||
|    * @param retain Whether to retain discovery messages. | ||||
|    */ | ||||
|   void set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, | ||||
|                           MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool clean = false); | ||||
|                           MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool discover_ip, | ||||
|                           bool clean = false); | ||||
|   /// Get Home Assistant discovery info. | ||||
|   const MQTTDiscoveryInfo &get_discovery_info() const; | ||||
|   /// Globally disable Home Assistant discovery. | ||||
|   void disable_discovery(); | ||||
|   bool is_discovery_enabled() const; | ||||
|   bool is_discovery_ip_enabled() const; | ||||
|  | ||||
| #if ASYNC_TCP_SSL_ENABLED | ||||
|   /** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker. | ||||
| @@ -290,6 +293,7 @@ class MQTTClientComponent : public Component { | ||||
|   MQTTDiscoveryInfo discovery_info_{ | ||||
|       .prefix = "homeassistant", | ||||
|       .retain = true, | ||||
|       .discover_ip = true, | ||||
|       .clean = false, | ||||
|       .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, | ||||
|       .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, | ||||
|   | ||||
| @@ -1,14 +1,17 @@ | ||||
| #ifdef USE_ESP32_FRAMEWORK_ARDUINO | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include "ota_backend_arduino_esp32.h" | ||||
| #include "ota_backend.h" | ||||
| #include "ota_backend_arduino_esp32.h" | ||||
|  | ||||
| #include <Update.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ota { | ||||
|  | ||||
| static const char *const TAG = "ota.arduino_esp32"; | ||||
|  | ||||
| std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoESP32OTABackend>(); } | ||||
|  | ||||
| OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) { | ||||
| @@ -20,6 +23,9 @@ OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) { | ||||
|   uint8_t error = Update.getError(); | ||||
|   if (error == UPDATE_ERROR_SIZE) | ||||
|     return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; | ||||
|  | ||||
|   ESP_LOGE(TAG, "Begin error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_UNKNOWN; | ||||
| } | ||||
|  | ||||
| @@ -27,16 +33,25 @@ void ArduinoESP32OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5 | ||||
|  | ||||
| OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) { | ||||
|   size_t written = Update.write(data, len); | ||||
|   if (written != len) { | ||||
|     return OTA_RESPONSE_ERROR_WRITING_FLASH; | ||||
|   if (written == len) { | ||||
|     return OTA_RESPONSE_OK; | ||||
|   } | ||||
|   return OTA_RESPONSE_OK; | ||||
|  | ||||
|   uint8_t error = Update.getError(); | ||||
|   ESP_LOGE(TAG, "Write error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_WRITING_FLASH; | ||||
| } | ||||
|  | ||||
| OTAResponseTypes ArduinoESP32OTABackend::end() { | ||||
|   if (!Update.end()) | ||||
|     return OTA_RESPONSE_ERROR_UPDATE_END; | ||||
|   return OTA_RESPONSE_OK; | ||||
|   if (Update.end()) { | ||||
|     return OTA_RESPONSE_OK; | ||||
|   } | ||||
|  | ||||
|   uint8_t error = Update.getError(); | ||||
|   ESP_LOGE(TAG, "End error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_UPDATE_END; | ||||
| } | ||||
|  | ||||
| void ArduinoESP32OTABackend::abort() { Update.abort(); } | ||||
|   | ||||
| @@ -1,16 +1,19 @@ | ||||
| #ifdef USE_ARDUINO | ||||
| #ifdef USE_ESP8266 | ||||
| #include "ota_backend.h" | ||||
| #include "ota_backend_arduino_esp8266.h" | ||||
| #include "ota_backend.h" | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/components/esp8266/preferences.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <Updater.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ota { | ||||
|  | ||||
| static const char *const TAG = "ota.arduino_esp8266"; | ||||
|  | ||||
| std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoESP8266OTABackend>(); } | ||||
|  | ||||
| OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { | ||||
| @@ -29,6 +32,9 @@ OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { | ||||
|     return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; | ||||
|   if (error == UPDATE_ERROR_SPACE) | ||||
|     return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; | ||||
|  | ||||
|   ESP_LOGE(TAG, "Begin error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_UNKNOWN; | ||||
| } | ||||
|  | ||||
| @@ -36,16 +42,25 @@ void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { Update.setMD5(m | ||||
|  | ||||
| OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) { | ||||
|   size_t written = Update.write(data, len); | ||||
|   if (written != len) { | ||||
|     return OTA_RESPONSE_ERROR_WRITING_FLASH; | ||||
|   if (written == len) { | ||||
|     return OTA_RESPONSE_OK; | ||||
|   } | ||||
|   return OTA_RESPONSE_OK; | ||||
|  | ||||
|   uint8_t error = Update.getError(); | ||||
|   ESP_LOGE(TAG, "Write error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_WRITING_FLASH; | ||||
| } | ||||
|  | ||||
| OTAResponseTypes ArduinoESP8266OTABackend::end() { | ||||
|   if (!Update.end()) | ||||
|     return OTA_RESPONSE_ERROR_UPDATE_END; | ||||
|   return OTA_RESPONSE_OK; | ||||
|   if (Update.end()) { | ||||
|     return OTA_RESPONSE_OK; | ||||
|   } | ||||
|  | ||||
|   uint8_t error = Update.getError(); | ||||
|   ESP_LOGE(TAG, "End error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_UPDATE_END; | ||||
| } | ||||
|  | ||||
| void ArduinoESP8266OTABackend::abort() { | ||||
|   | ||||
| @@ -1,14 +1,17 @@ | ||||
| #ifdef USE_LIBRETINY | ||||
| #include "ota_backend.h" | ||||
| #include "ota_backend_arduino_libretiny.h" | ||||
| #include "ota_backend.h" | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <Update.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ota { | ||||
|  | ||||
| static const char *const TAG = "ota.arduino_libretiny"; | ||||
|  | ||||
| std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoLibreTinyOTABackend>(); } | ||||
|  | ||||
| OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { | ||||
| @@ -20,6 +23,9 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { | ||||
|   uint8_t error = Update.getError(); | ||||
|   if (error == UPDATE_ERROR_SIZE) | ||||
|     return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; | ||||
|  | ||||
|   ESP_LOGE(TAG, "Begin error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_UNKNOWN; | ||||
| } | ||||
|  | ||||
| @@ -27,16 +33,25 @@ void ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) { Update.setMD5 | ||||
|  | ||||
| OTAResponseTypes ArduinoLibreTinyOTABackend::write(uint8_t *data, size_t len) { | ||||
|   size_t written = Update.write(data, len); | ||||
|   if (written != len) { | ||||
|     return OTA_RESPONSE_ERROR_WRITING_FLASH; | ||||
|   if (written == len) { | ||||
|     return OTA_RESPONSE_OK; | ||||
|   } | ||||
|   return OTA_RESPONSE_OK; | ||||
|  | ||||
|   uint8_t error = Update.getError(); | ||||
|   ESP_LOGE(TAG, "Write error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_WRITING_FLASH; | ||||
| } | ||||
|  | ||||
| OTAResponseTypes ArduinoLibreTinyOTABackend::end() { | ||||
|   if (!Update.end()) | ||||
|     return OTA_RESPONSE_ERROR_UPDATE_END; | ||||
|   return OTA_RESPONSE_OK; | ||||
|   if (Update.end()) { | ||||
|     return OTA_RESPONSE_OK; | ||||
|   } | ||||
|  | ||||
|   uint8_t error = Update.getError(); | ||||
|   ESP_LOGE(TAG, "End error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_UPDATE_END; | ||||
| } | ||||
|  | ||||
| void ArduinoLibreTinyOTABackend::abort() { Update.abort(); } | ||||
|   | ||||
| @@ -1,16 +1,19 @@ | ||||
| #ifdef USE_ARDUINO | ||||
| #ifdef USE_RP2040 | ||||
| #include "ota_backend.h" | ||||
| #include "ota_backend_arduino_rp2040.h" | ||||
| #include "ota_backend.h" | ||||
|  | ||||
| #include "esphome/components/rp2040/preferences.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <Updater.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ota { | ||||
|  | ||||
| static const char *const TAG = "ota.arduino_rp2040"; | ||||
|  | ||||
| std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoRP2040OTABackend>(); } | ||||
|  | ||||
| OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { | ||||
| @@ -29,6 +32,9 @@ OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { | ||||
|     return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; | ||||
|   if (error == UPDATE_ERROR_SPACE) | ||||
|     return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE; | ||||
|  | ||||
|   ESP_LOGE(TAG, "Begin error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_UNKNOWN; | ||||
| } | ||||
|  | ||||
| @@ -36,16 +42,25 @@ void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { Update.setMD5(md | ||||
|  | ||||
| OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) { | ||||
|   size_t written = Update.write(data, len); | ||||
|   if (written != len) { | ||||
|     return OTA_RESPONSE_ERROR_WRITING_FLASH; | ||||
|   if (written == len) { | ||||
|     return OTA_RESPONSE_OK; | ||||
|   } | ||||
|   return OTA_RESPONSE_OK; | ||||
|  | ||||
|   uint8_t error = Update.getError(); | ||||
|   ESP_LOGE(TAG, "Write error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_WRITING_FLASH; | ||||
| } | ||||
|  | ||||
| OTAResponseTypes ArduinoRP2040OTABackend::end() { | ||||
|   if (!Update.end()) | ||||
|     return OTA_RESPONSE_ERROR_UPDATE_END; | ||||
|   return OTA_RESPONSE_OK; | ||||
|   if (Update.end()) { | ||||
|     return OTA_RESPONSE_OK; | ||||
|   } | ||||
|  | ||||
|   uint8_t error = Update.getError(); | ||||
|   ESP_LOGE(TAG, "End error: %d", error); | ||||
|  | ||||
|   return OTA_RESPONSE_ERROR_UPDATE_END; | ||||
| } | ||||
|  | ||||
| void ArduinoRP2040OTABackend::abort() { | ||||
|   | ||||
| @@ -72,43 +72,44 @@ void PMWCS3Component::dump_config() { | ||||
|   LOG_SENSOR("  ", "vwc", this->vwc_sensor_); | ||||
| } | ||||
| void PMWCS3Component::read_data_() { | ||||
|   uint8_t data[8]; | ||||
|   float e25, ec, temperature, vwc; | ||||
|  | ||||
|   /////// Super important !!!! first activate reading PMWCS3_REG_READ_START (if not, return always the same values) //// | ||||
|  | ||||
|   if (!this->write_bytes(PMWCS3_REG_READ_START, nullptr, 0)) { | ||||
|     this->status_set_warning(); | ||||
|     ESP_LOGVV(TAG, "Failed to write into REG_READ_START register !!!"); | ||||
|     return; | ||||
|   } | ||||
|   // NOLINT  delay(100); | ||||
|  | ||||
|   if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { | ||||
|     ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (this->e25_sensor_ != nullptr) { | ||||
|     e25 = ((data[1] << 8) | data[0]) / 100.0; | ||||
|     this->e25_sensor_->publish_state(e25); | ||||
|     ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25); | ||||
|   } | ||||
|   if (this->ec_sensor_ != nullptr) { | ||||
|     ec = ((data[3] << 8) | data[2]) / 10.0; | ||||
|     this->ec_sensor_->publish_state(ec); | ||||
|     ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec); | ||||
|   } | ||||
|   if (this->temperature_sensor_ != nullptr) { | ||||
|     temperature = ((data[5] << 8) | data[4]) / 100.0; | ||||
|     this->temperature_sensor_->publish_state(temperature); | ||||
|     ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature); | ||||
|   } | ||||
|   if (this->vwc_sensor_ != nullptr) { | ||||
|     vwc = ((data[7] << 8) | data[6]) / 10.0; | ||||
|     this->vwc_sensor_->publish_state(vwc); | ||||
|     ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); | ||||
|   } | ||||
|   // Wait for the sensor to be ready. | ||||
|   // 80ms empirically determined (conservative). | ||||
|   this->set_timeout(80, [this] { | ||||
|     uint8_t data[8]; | ||||
|     float e25, ec, temperature, vwc; | ||||
|     if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { | ||||
|       ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|     if (this->e25_sensor_ != nullptr) { | ||||
|       e25 = ((data[1] << 8) | data[0]) / 100.0; | ||||
|       this->e25_sensor_->publish_state(e25); | ||||
|       ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25); | ||||
|     } | ||||
|     if (this->ec_sensor_ != nullptr) { | ||||
|       ec = ((data[3] << 8) | data[2]) / 10.0; | ||||
|       this->ec_sensor_->publish_state(ec); | ||||
|       ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec); | ||||
|     } | ||||
|     if (this->temperature_sensor_ != nullptr) { | ||||
|       temperature = ((data[5] << 8) | data[4]) / 100.0; | ||||
|       this->temperature_sensor_->publish_state(temperature); | ||||
|       ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature); | ||||
|     } | ||||
|     if (this->vwc_sensor_ != nullptr) { | ||||
|       vwc = ((data[7] << 8) | data[6]) / 10.0; | ||||
|       this->vwc_sensor_->publish_state(vwc); | ||||
|       ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| }  // namespace pmwcs3 | ||||
|   | ||||
| @@ -27,7 +27,7 @@ bool SmlFile::setup_node(SmlNode *node) { | ||||
|   uint8_t parse_length = length; | ||||
|   if (has_extended_length) { | ||||
|     length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); | ||||
|     parse_length = length - 1; | ||||
|     parse_length = length; | ||||
|     this->pos_ += 1; | ||||
|   } | ||||
|  | ||||
| @@ -37,7 +37,9 @@ bool SmlFile::setup_node(SmlNode *node) { | ||||
|   node->type = type & 0x07; | ||||
|   node->nodes.clear(); | ||||
|   node->value_bytes.clear(); | ||||
|   if (this->buffer_[this->pos_] == 0x00) {  // end of message | ||||
|  | ||||
|   // if the list is a has_extended_length list with e.g. 16 elements this is a 0x00 byte but not the end of message | ||||
|   if (!has_extended_length && this->buffer_[this->pos_] == 0x00) {  // end of message | ||||
|     this->pos_ += 1; | ||||
|   } else if (is_list) {  // list | ||||
|     this->pos_ += 1; | ||||
|   | ||||
| @@ -5,8 +5,8 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifndef __linux__ | ||||
| #error This HostUartComponent implementation is only for Linux | ||||
| #if !(defined(__linux__) || defined(__APPLE__)) | ||||
| #error This HostUartComponent implementation is not supported on this host OS | ||||
| #endif | ||||
|  | ||||
| #include <stdio.h> | ||||
| @@ -24,6 +24,9 @@ | ||||
| namespace { | ||||
|  | ||||
| speed_t get_baud(int baud) { | ||||
| #ifdef __APPLE__ | ||||
|   return baud; | ||||
| #else | ||||
|   switch (baud) { | ||||
|     case 50: | ||||
|       return B50; | ||||
| @@ -88,6 +91,7 @@ speed_t get_baud(int baud) { | ||||
|     default: | ||||
|       return B0; | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| }  // namespace | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -46,29 +46,6 @@ static const char *const HEADER_CORS_REQ_PNA = "Access-Control-Request-Private-N | ||||
| static const char *const HEADER_CORS_ALLOW_PNA = "Access-Control-Allow-Private-Network"; | ||||
| #endif | ||||
|  | ||||
| #if USE_WEBSERVER_VERSION == 1 | ||||
| void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, | ||||
|                const std::function<void(AsyncResponseStream &stream, EntityBase *obj)> &action_func = nullptr) { | ||||
|   stream->print("<tr class=\""); | ||||
|   stream->print(klass.c_str()); | ||||
|   if (obj->is_internal()) | ||||
|     stream->print(" internal"); | ||||
|   stream->print("\" id=\""); | ||||
|   stream->print(klass.c_str()); | ||||
|   stream->print("-"); | ||||
|   stream->print(obj->get_object_id().c_str()); | ||||
|   stream->print("\"><td>"); | ||||
|   stream->print(obj->get_name().c_str()); | ||||
|   stream->print("</td><td></td><td>"); | ||||
|   stream->print(action.c_str()); | ||||
|   if (action_func) { | ||||
|     action_func(*stream, obj); | ||||
|   } | ||||
|   stream->print("</td>"); | ||||
|   stream->print("</tr>"); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| UrlMatch match_url(const std::string &url, bool only_domain = false) { | ||||
|   UrlMatch match; | ||||
|   match.valid = false; | ||||
| @@ -102,11 +79,6 @@ WebServer::WebServer(web_server_base::WebServerBase *base) | ||||
| #endif | ||||
| } | ||||
|  | ||||
| #if USE_WEBSERVER_VERSION == 1 | ||||
| void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } | ||||
| void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||
| void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; } | ||||
| #endif | ||||
| @@ -181,187 +153,6 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { | ||||
|   response->addHeader("Content-Encoding", "gzip"); | ||||
|   request->send(response); | ||||
| } | ||||
| #elif USE_WEBSERVER_VERSION == 1 | ||||
| void WebServer::handle_index_request(AsyncWebServerRequest *request) { | ||||
|   AsyncResponseStream *stream = request->beginResponseStream("text/html"); | ||||
|   const std::string &title = App.get_name(); | ||||
|   stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta " | ||||
|                   "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>")); | ||||
|   stream->print(title.c_str()); | ||||
|   stream->print(F("</title>")); | ||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||
|   stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">")); | ||||
| #endif | ||||
|   if (strlen(this->css_url_) > 0) { | ||||
|     stream->print(F(R"(<link rel="stylesheet" href=")")); | ||||
|     stream->print(this->css_url_); | ||||
|     stream->print(F("\">")); | ||||
|   } | ||||
|   stream->print(F("</head><body>")); | ||||
|   stream->print(F("<article class=\"markdown-body\"><h1>")); | ||||
|   stream->print(title.c_str()); | ||||
|   stream->print(F("</h1>")); | ||||
|   stream->print(F("<h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>")); | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
|   for (auto *obj : App.get_sensors()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "sensor", ""); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SWITCH | ||||
|   for (auto *obj : App.get_switches()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "switch", "<button>Toggle</button>"); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BUTTON | ||||
|   for (auto *obj : App.get_buttons()) | ||||
|     write_row(stream, obj, "button", "<button>Press</button>"); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   for (auto *obj : App.get_binary_sensors()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "binary_sensor", ""); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_FAN | ||||
|   for (auto *obj : App.get_fans()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "fan", "<button>Toggle</button>"); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIGHT | ||||
|   for (auto *obj : App.get_lights()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "light", "<button>Toggle</button>"); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   for (auto *obj : App.get_text_sensors()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "text_sensor", ""); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_COVER | ||||
|   for (auto *obj : App.get_covers()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "cover", "<button>Open</button><button>Close</button>"); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
|   for (auto *obj : App.get_numbers()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) { | ||||
|       write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) { | ||||
|         number::Number *number = (number::Number *) obj; | ||||
|         stream.print(R"(<input type="number" min=")"); | ||||
|         stream.print(number->traits.get_min_value()); | ||||
|         stream.print(R"(" max=")"); | ||||
|         stream.print(number->traits.get_max_value()); | ||||
|         stream.print(R"(" step=")"); | ||||
|         stream.print(number->traits.get_step()); | ||||
|         stream.print(R"(" value=")"); | ||||
|         stream.print(number->state); | ||||
|         stream.print(R"("/>)"); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT | ||||
|   for (auto *obj : App.get_texts()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) { | ||||
|       write_row(stream, obj, "text", "", [](AsyncResponseStream &stream, EntityBase *obj) { | ||||
|         text::Text *text = (text::Text *) obj; | ||||
|         auto mode = (int) text->traits.get_mode(); | ||||
|         stream.print(R"(<input type=")"); | ||||
|         if (mode == 2) { | ||||
|           stream.print(R"(password)"); | ||||
|         } else {  // default | ||||
|           stream.print(R"(text)"); | ||||
|         } | ||||
|         stream.print(R"(" minlength=")"); | ||||
|         stream.print(text->traits.get_min_length()); | ||||
|         stream.print(R"(" maxlength=")"); | ||||
|         stream.print(text->traits.get_max_length()); | ||||
|         stream.print(R"(" pattern=")"); | ||||
|         stream.print(text->traits.get_pattern().c_str()); | ||||
|         stream.print(R"(" value=")"); | ||||
|         stream.print(text->state.c_str()); | ||||
|         stream.print(R"("/>)"); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SELECT | ||||
|   for (auto *obj : App.get_selects()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) { | ||||
|       write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) { | ||||
|         select::Select *select = (select::Select *) obj; | ||||
|         stream.print("<select>"); | ||||
|         stream.print("<option></option>"); | ||||
|         for (auto const &option : select->traits.get_options()) { | ||||
|           stream.print("<option>"); | ||||
|           stream.print(option.c_str()); | ||||
|           stream.print("</option>"); | ||||
|         } | ||||
|         stream.print("</select>"); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LOCK | ||||
|   for (auto *obj : App.get_locks()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) { | ||||
|       write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) { | ||||
|         lock::Lock *lock = (lock::Lock *) obj; | ||||
|         stream.print("<button>Lock</button><button>Unlock</button>"); | ||||
|         if (lock->traits.get_supports_open()) { | ||||
|           stream.print("<button>Open</button>"); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CLIMATE | ||||
|   for (auto *obj : App.get_climates()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "climate", ""); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for " | ||||
|                   "REST API documentation.</p>")); | ||||
|   if (this->allow_ota_) { | ||||
|     stream->print( | ||||
|         F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input " | ||||
|           "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>")); | ||||
|   } | ||||
|   stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>")); | ||||
| #ifdef USE_WEBSERVER_JS_INCLUDE | ||||
|   if (this->js_include_ != nullptr) { | ||||
|     stream->print(F("<script type=\"module\" src=\"/0.js\"></script>")); | ||||
|   } | ||||
| #endif | ||||
|   if (strlen(this->js_url_) > 0) { | ||||
|     stream->print(F("<script src=\"")); | ||||
|     stream->print(this->js_url_); | ||||
|     stream->print(F("\"></script>")); | ||||
|   } | ||||
|   stream->print(F("</article></body></html>")); | ||||
|   request->send(stream); | ||||
| } | ||||
| #elif USE_WEBSERVER_VERSION >= 2 | ||||
| void WebServer::handle_index_request(AsyncWebServerRequest *request) { | ||||
|   AsyncWebServerResponse *response = | ||||
|   | ||||
							
								
								
									
										217
									
								
								esphome/components/web_server/web_server_v1.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								esphome/components/web_server/web_server_v1.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,217 @@ | ||||
| #include "web_server.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| #if USE_WEBSERVER_VERSION == 1 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace web_server { | ||||
|  | ||||
| void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, | ||||
|                const std::function<void(AsyncResponseStream &stream, EntityBase *obj)> &action_func = nullptr) { | ||||
|   stream->print("<tr class=\""); | ||||
|   stream->print(klass.c_str()); | ||||
|   if (obj->is_internal()) | ||||
|     stream->print(" internal"); | ||||
|   stream->print("\" id=\""); | ||||
|   stream->print(klass.c_str()); | ||||
|   stream->print("-"); | ||||
|   stream->print(obj->get_object_id().c_str()); | ||||
|   stream->print("\"><td>"); | ||||
|   stream->print(obj->get_name().c_str()); | ||||
|   stream->print("</td><td></td><td>"); | ||||
|   stream->print(action.c_str()); | ||||
|   if (action_func) { | ||||
|     action_func(*stream, obj); | ||||
|   } | ||||
|   stream->print("</td>"); | ||||
|   stream->print("</tr>"); | ||||
| } | ||||
|  | ||||
| void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } | ||||
|  | ||||
| void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } | ||||
|  | ||||
| void WebServer::handle_index_request(AsyncWebServerRequest *request) { | ||||
|   AsyncResponseStream *stream = request->beginResponseStream("text/html"); | ||||
|   const std::string &title = App.get_name(); | ||||
|   stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta " | ||||
|                   "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>")); | ||||
|   stream->print(title.c_str()); | ||||
|   stream->print(F("</title>")); | ||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||
|   stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">")); | ||||
| #endif | ||||
|   if (strlen(this->css_url_) > 0) { | ||||
|     stream->print(F(R"(<link rel="stylesheet" href=")")); | ||||
|     stream->print(this->css_url_); | ||||
|     stream->print(F("\">")); | ||||
|   } | ||||
|   stream->print(F("</head><body>")); | ||||
|   stream->print(F("<article class=\"markdown-body\"><h1>")); | ||||
|   stream->print(title.c_str()); | ||||
|   stream->print(F("</h1>")); | ||||
|   stream->print(F("<h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>")); | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
|   for (auto *obj : App.get_sensors()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "sensor", ""); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SWITCH | ||||
|   for (auto *obj : App.get_switches()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "switch", "<button>Toggle</button>"); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BUTTON | ||||
|   for (auto *obj : App.get_buttons()) | ||||
|     write_row(stream, obj, "button", "<button>Press</button>"); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   for (auto *obj : App.get_binary_sensors()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "binary_sensor", ""); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_FAN | ||||
|   for (auto *obj : App.get_fans()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "fan", "<button>Toggle</button>"); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIGHT | ||||
|   for (auto *obj : App.get_lights()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "light", "<button>Toggle</button>"); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   for (auto *obj : App.get_text_sensors()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "text_sensor", ""); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_COVER | ||||
|   for (auto *obj : App.get_covers()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "cover", "<button>Open</button><button>Close</button>"); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
|   for (auto *obj : App.get_numbers()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) { | ||||
|       write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) { | ||||
|         number::Number *number = (number::Number *) obj; | ||||
|         stream.print(R"(<input type="number" min=")"); | ||||
|         stream.print(number->traits.get_min_value()); | ||||
|         stream.print(R"(" max=")"); | ||||
|         stream.print(number->traits.get_max_value()); | ||||
|         stream.print(R"(" step=")"); | ||||
|         stream.print(number->traits.get_step()); | ||||
|         stream.print(R"(" value=")"); | ||||
|         stream.print(number->state); | ||||
|         stream.print(R"("/>)"); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT | ||||
|   for (auto *obj : App.get_texts()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) { | ||||
|       write_row(stream, obj, "text", "", [](AsyncResponseStream &stream, EntityBase *obj) { | ||||
|         text::Text *text = (text::Text *) obj; | ||||
|         auto mode = (int) text->traits.get_mode(); | ||||
|         stream.print(R"(<input type=")"); | ||||
|         if (mode == 2) { | ||||
|           stream.print(R"(password)"); | ||||
|         } else {  // default | ||||
|           stream.print(R"(text)"); | ||||
|         } | ||||
|         stream.print(R"(" minlength=")"); | ||||
|         stream.print(text->traits.get_min_length()); | ||||
|         stream.print(R"(" maxlength=")"); | ||||
|         stream.print(text->traits.get_max_length()); | ||||
|         stream.print(R"(" pattern=")"); | ||||
|         stream.print(text->traits.get_pattern().c_str()); | ||||
|         stream.print(R"(" value=")"); | ||||
|         stream.print(text->state.c_str()); | ||||
|         stream.print(R"("/>)"); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SELECT | ||||
|   for (auto *obj : App.get_selects()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) { | ||||
|       write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) { | ||||
|         select::Select *select = (select::Select *) obj; | ||||
|         stream.print("<select>"); | ||||
|         stream.print("<option></option>"); | ||||
|         for (auto const &option : select->traits.get_options()) { | ||||
|           stream.print("<option>"); | ||||
|           stream.print(option.c_str()); | ||||
|           stream.print("</option>"); | ||||
|         } | ||||
|         stream.print("</select>"); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LOCK | ||||
|   for (auto *obj : App.get_locks()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) { | ||||
|       write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) { | ||||
|         lock::Lock *lock = (lock::Lock *) obj; | ||||
|         stream.print("<button>Lock</button><button>Unlock</button>"); | ||||
|         if (lock->traits.get_supports_open()) { | ||||
|           stream.print("<button>Open</button>"); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CLIMATE | ||||
|   for (auto *obj : App.get_climates()) { | ||||
|     if (this->include_internal_ || !obj->is_internal()) | ||||
|       write_row(stream, obj, "climate", ""); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for " | ||||
|                   "REST API documentation.</p>")); | ||||
|   if (this->allow_ota_) { | ||||
|     stream->print( | ||||
|         F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input " | ||||
|           "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>")); | ||||
|   } | ||||
|   stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>")); | ||||
| #ifdef USE_WEBSERVER_JS_INCLUDE | ||||
|   if (this->js_include_ != nullptr) { | ||||
|     stream->print(F("<script type=\"module\" src=\"/0.js\"></script>")); | ||||
|   } | ||||
| #endif | ||||
|   if (strlen(this->js_url_) > 0) { | ||||
|     stream->print(F("<script src=\"")); | ||||
|     stream->print(this->js_url_); | ||||
|     stream->print(F("\"></script>")); | ||||
|   } | ||||
|   stream->print(F("</article></body></html>")); | ||||
|   request->send(stream); | ||||
| } | ||||
|  | ||||
| }  // namespace web_server | ||||
| }  // namespace esphome | ||||
| #endif | ||||
| @@ -82,8 +82,8 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) { | ||||
|  | ||||
|   // WiFiClass::mode above calls esp_netif_create_default_wifi_sta() and | ||||
|   // esp_netif_create_default_wifi_ap(), which creates the interfaces. | ||||
|   if (set_sta) | ||||
|     s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); | ||||
|   // s_sta_netif handle is set during ESPHOME_EVENT_ID_WIFI_STA_START event | ||||
|  | ||||
| #ifdef USE_WIFI_AP | ||||
|   if (set_ap) | ||||
|     s_ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); | ||||
| @@ -495,6 +495,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ | ||||
|     case ESPHOME_EVENT_ID_WIFI_STA_START: { | ||||
|       ESP_LOGV(TAG, "Event: WiFi STA start"); | ||||
|       // apply hostname | ||||
|       s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); | ||||
|       esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str()); | ||||
|       if (err != ERR_OK) { | ||||
|         ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); | ||||
|   | ||||
| @@ -829,7 +829,6 @@ def time_of_day(value): | ||||
|  | ||||
|  | ||||
| def date_time(date: bool, time: bool): | ||||
|  | ||||
|     pattern_str = r"^"  # Start of string | ||||
|     if date: | ||||
|         pattern_str += r"\d{4}-\d{1,2}-\d{1,2}" | ||||
| @@ -2031,6 +2030,7 @@ def require_framework_version( | ||||
|     esp32_arduino=None, | ||||
|     esp8266_arduino=None, | ||||
|     rp2040_arduino=None, | ||||
|     host=None, | ||||
|     max_version=False, | ||||
|     extra_message=None, | ||||
| ): | ||||
| @@ -2065,6 +2065,13 @@ def require_framework_version( | ||||
|                     msg += f". {extra_message}" | ||||
|                 raise Invalid(msg) | ||||
|             required = rp2040_arduino | ||||
|         elif CORE.is_host and framework == "host": | ||||
|             if host is None: | ||||
|                 msg = "This feature is incompatible with host platform" | ||||
|                 if extra_message: | ||||
|                     msg += f". {extra_message}" | ||||
|                 raise Invalid(msg) | ||||
|             required = host | ||||
|         else: | ||||
|             raise Invalid( | ||||
|                 f""" | ||||
|   | ||||
| @@ -38,6 +38,9 @@ | ||||
| #define USE_LIGHT | ||||
| #define USE_LOCK | ||||
| #define USE_LOGGER | ||||
| #define USE_LVGL | ||||
| #define USE_LVGL_FONT | ||||
| #define USE_LVGL_IMAGE | ||||
| #define USE_MDNS | ||||
| #define USE_MEDIA_PLAYER | ||||
| #define USE_MQTT | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| import logging | ||||
| from typing import Callable, Optional, Any, ContextManager | ||||
| from types import ModuleType | ||||
| import importlib | ||||
| import importlib.util | ||||
| import importlib.resources | ||||
| import importlib.abc | ||||
| import sys | ||||
| from pathlib import Path | ||||
| from dataclasses import dataclass | ||||
| import importlib | ||||
| import importlib.abc | ||||
| import importlib.resources | ||||
| import importlib.util | ||||
| import logging | ||||
| from pathlib import Path | ||||
| import sys | ||||
| from types import ModuleType | ||||
| from typing import Any, Callable, ContextManager, Optional | ||||
|  | ||||
| from esphome.const import SOURCE_FILE_EXTENSIONS | ||||
| import esphome.core.config | ||||
| from esphome.core import CORE | ||||
| import esphome.core.config | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| @@ -175,7 +175,11 @@ def _lookup_module(domain): | ||||
|     try: | ||||
|         module = importlib.import_module(f"esphome.components.{domain}") | ||||
|     except ImportError as e: | ||||
|         if "No module named" not in str(e): | ||||
|         if "No module named" in str(e): | ||||
|             _LOGGER.error( | ||||
|                 "Unable to import component %s: %s", domain, str(e), exc_info=False | ||||
|             ) | ||||
|         else: | ||||
|             _LOGGER.error("Unable to import component %s:", domain, exc_info=True) | ||||
|         return None | ||||
|     except Exception:  # pylint: disable=broad-except | ||||
|   | ||||
| @@ -42,6 +42,7 @@ lib_deps = | ||||
|     pavlodn/HaierProtocol@0.9.31           ; haier | ||||
|     ; This is using the repository until a new release is published to PlatformIO | ||||
|     https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library | ||||
|     lvgl/lvgl@8.4.0                                       ; lvgl | ||||
| build_flags = | ||||
|     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE | ||||
| src_filter = | ||||
|   | ||||
| @@ -105,3 +105,33 @@ disable = [ | ||||
|  | ||||
| [tool.pylint.FORMAT] | ||||
| expected-line-ending-format = "LF" | ||||
|  | ||||
| [tool.ruff] | ||||
| required-version = ">=0.5.0" | ||||
|  | ||||
| [tool.ruff.lint] | ||||
| select = [ | ||||
|   "E", # pycodestyle | ||||
|   "F", # pyflakes/autoflake | ||||
|   "I", # isort | ||||
|   "PL", # pylint | ||||
|   "UP", # pyupgrade | ||||
| ] | ||||
|  | ||||
| ignore = [ | ||||
|   "E501", # line too long | ||||
|   "PLR0911", # Too many return statements ({returns} > {max_returns}) | ||||
|   "PLR0912", # Too many branches ({branches} > {max_branches}) | ||||
|   "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) | ||||
|   "PLR0915", # Too many statements ({statements} > {max_statements}) | ||||
|   "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable | ||||
|   "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target | ||||
| ] | ||||
|  | ||||
| [tool.ruff.lint.isort] | ||||
| force-sort-within-sections = true | ||||
| known-first-party = [ | ||||
|   "esphome", | ||||
| ] | ||||
| combine-as-imports = true | ||||
| split-on-trailing-comma = false | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| async_timeout==4.0.3; python_version <= "3.10" | ||||
| cryptography==42.0.2 | ||||
| cryptography==43.0.0 | ||||
| voluptuous==0.14.2 | ||||
| PyYAML==6.0.1 | ||||
| paho-mqtt==1.6.1 | ||||
| @@ -13,7 +13,7 @@ platformio==6.1.15  # When updating platformio, also update Dockerfile | ||||
| esptool==4.7.0 | ||||
| click==8.1.7 | ||||
| esphome-dashboard==20240620.0 | ||||
| aioesphomeapi==24.3.0 | ||||
| aioesphomeapi==24.6.2 | ||||
| zeroconf==0.132.2 | ||||
| python-magic==0.4.27 | ||||
| ruamel.yaml==0.18.6 # dashboard_import | ||||
|   | ||||
							
								
								
									
										12
									
								
								tests/components/apds9306/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								tests/components/apds9306/common.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| i2c: | ||||
|   - id: i2c_apds9306 | ||||
|     scl: ${scl_pin} | ||||
|     sda: ${sda_pin} | ||||
|  | ||||
| sensor: | ||||
|   - platform: apds9306 | ||||
|     name: "APDS9306 Light Level" | ||||
|     gain: 3 | ||||
|     bit_width: 16 | ||||
|     measurement_rate: 2000ms | ||||
|     update_interval: 60s | ||||
							
								
								
									
										5
									
								
								tests/components/apds9306/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/apds9306/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| substitutions: | ||||
|   scl_pin: GPIO22 | ||||
|   sda_pin: GPIO21 | ||||
|  | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										5
									
								
								tests/components/apds9306/test.esp32-c3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/apds9306/test.esp32-c3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| substitutions: | ||||
|   scl_pin: GPIO5 | ||||
|   sda_pin: GPIO4 | ||||
|  | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										5
									
								
								tests/components/apds9306/test.esp32-c3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/apds9306/test.esp32-c3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| substitutions: | ||||
|   scl_pin: GPIO5 | ||||
|   sda_pin: GPIO4 | ||||
|  | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										5
									
								
								tests/components/apds9306/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/apds9306/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| substitutions: | ||||
|   scl_pin: GPIO22 | ||||
|   sda_pin: GPIO21 | ||||
|  | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										5
									
								
								tests/components/apds9306/test.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/apds9306/test.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| substitutions: | ||||
|   scl_pin: GPIO5 | ||||
|   sda_pin: GPIO4 | ||||
|  | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										5
									
								
								tests/components/apds9306/test.rp2040-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/components/apds9306/test.rp2040-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| substitutions: | ||||
|   scl_pin: GPIO5 | ||||
|   sda_pin: GPIO4 | ||||
|  | ||||
| <<: !include common.yaml | ||||
| @@ -3,6 +3,13 @@ remote_transmitter: | ||||
|   carrier_duty_percent: 50% | ||||
|  | ||||
| climate: | ||||
|   - platform: heatpumpir | ||||
|     protocol: mitsubishi_heavy_zm | ||||
|     horizontal_default: left | ||||
|     vertical_default: up | ||||
|     name: HeatpumpIR Climate | ||||
|     min_temperature: 18 | ||||
|     max_temperature: 30 | ||||
|   - platform: heatpumpir | ||||
|     protocol: daikin | ||||
|     horizontal_default: mleft | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user