mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	| @@ -31,7 +31,7 @@ | ||||
|         "ms-python.python", | ||||
|         "ms-python.pylint", | ||||
|         "ms-python.flake8", | ||||
|         "ms-python.black-formatter", | ||||
|         "charliermarsh.ruff", | ||||
|         "visualstudioexptteam.vscodeintellicode", | ||||
|         // yaml | ||||
|         "redhat.vscode-yaml", | ||||
| @@ -49,14 +49,11 @@ | ||||
|         "flake8.args": [ | ||||
|           "--config=${workspaceFolder}/.flake8" | ||||
|         ], | ||||
|         "black-formatter.args": [ | ||||
|           "--config", | ||||
|           "${workspaceFolder}/pyproject.toml" | ||||
|         ], | ||||
|         "ruff.configuration": "${workspaceFolder}/pyproject.toml", | ||||
|         "[python]": { | ||||
|           // VS will say "Value is not accepted" before building the devcontainer, but the warning | ||||
|           // should go away after build is completed. | ||||
|           "editor.defaultFormatter": "ms-python.black-formatter" | ||||
|           "editor.defaultFormatter": "charliermarsh.ruff" | ||||
|         }, | ||||
|         "editor.formatOnPaste": false, | ||||
|         "editor.formatOnSave": true, | ||||
|   | ||||
							
								
								
									
										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.13.0 | ||||
|       uses: docker/build-push-action@v6.15.0 | ||||
|       env: | ||||
|         DOCKER_BUILD_SUMMARY: false | ||||
|         DOCKER_BUILD_RECORD_UPLOAD: false | ||||
| @@ -72,7 +72,7 @@ runs: | ||||
|  | ||||
|     - name: Build and push to dockerhub by digest | ||||
|       id: build-dockerhub | ||||
|       uses: docker/build-push-action@v6.13.0 | ||||
|       uses: docker/build-push-action@v6.15.0 | ||||
|       env: | ||||
|         DOCKER_BUILD_SUMMARY: false | ||||
|         DOCKER_BUILD_RECORD_UPLOAD: false | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							| @@ -22,7 +22,7 @@ runs: | ||||
|         python-version: ${{ inputs.python-version }} | ||||
|     - name: Restore Python virtual environment | ||||
|       id: cache-venv | ||||
|       uses: actions/cache/restore@v4.2.0 | ||||
|       uses: actions/cache/restore@v4.2.2 | ||||
|       with: | ||||
|         path: venv | ||||
|         # yamllint disable-line rule:line-length | ||||
|   | ||||
							
								
								
									
										10
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -33,11 +33,11 @@ concurrency: | ||||
| jobs: | ||||
|   check-docker: | ||||
|     name: Build docker containers | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         arch: [amd64, aarch64] | ||||
|         os: ["ubuntu-latest", "ubuntu-24.04-arm"] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4.1.7 | ||||
| @@ -46,9 +46,7 @@ jobs: | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.9.0 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v3.4.0 | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|  | ||||
|       - name: Set TAG | ||||
|         run: | | ||||
| @@ -58,6 +56,6 @@ jobs: | ||||
|         run: | | ||||
|           docker/build.py \ | ||||
|             --tag "${TAG}" \ | ||||
|             --arch "${{ matrix.arch }}" \ | ||||
|             --arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \ | ||||
|             --build-type "${{ matrix.build_type }}" \ | ||||
|             build | ||||
|   | ||||
							
								
								
									
										18
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -47,7 +47,7 @@ jobs: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|       - name: Restore Python virtual environment | ||||
|         id: cache-venv | ||||
|         uses: actions/cache@v4.2.0 | ||||
|         uses: actions/cache@v4.2.2 | ||||
|         with: | ||||
|           path: venv | ||||
|           # yamllint disable-line rule:line-length | ||||
| @@ -61,8 +61,8 @@ jobs: | ||||
|           pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt | ||||
|           pip install -e . | ||||
|  | ||||
|   black: | ||||
|     name: Check black | ||||
|   ruff: | ||||
|     name: Check ruff | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
| @@ -74,10 +74,10 @@ jobs: | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Run black | ||||
|       - name: Run Ruff | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           black --verbose esphome tests | ||||
|           ruff format esphome tests | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
| @@ -255,7 +255,7 @@ jobs: | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - black | ||||
|       - ruff | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
| @@ -303,14 +303,14 @@ jobs: | ||||
|  | ||||
|       - name: Cache platformio | ||||
|         if: github.ref == 'refs/heads/dev' | ||||
|         uses: actions/cache@v4.2.0 | ||||
|         uses: actions/cache@v4.2.2 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-${{ matrix.pio_cache_key }} | ||||
|  | ||||
|       - name: Cache platformio | ||||
|         if: github.ref != 'refs/heads/dev' | ||||
|         uses: actions/cache/restore@v4.2.0 | ||||
|         uses: actions/cache/restore@v4.2.2 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-${{ matrix.pio_cache_key }} | ||||
| @@ -482,7 +482,7 @@ jobs: | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - black | ||||
|       - ruff | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/matchers/lint-python.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/matchers/lint-python.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,11 +1,11 @@ | ||||
| { | ||||
|   "problemMatcher": [ | ||||
|     { | ||||
|       "owner": "black", | ||||
|       "owner": "ruff", | ||||
|       "severity": "error", | ||||
|       "pattern": [ | ||||
|         { | ||||
|           "regexp": "^(.*): (Please format this file with the black formatter)", | ||||
|           "regexp": "^(.*): (Please format this file with the ruff formatter)", | ||||
|           "file": 1, | ||||
|           "message": 2 | ||||
|         } | ||||
|   | ||||
							
								
								
									
										10
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -89,10 +89,10 @@ jobs: | ||||
|           python-version: "3.9" | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.9.0 | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|       - name: Set up QEMU | ||||
|         if: matrix.platform != 'linux/amd64' | ||||
|         uses: docker/setup-qemu-action@v3.4.0 | ||||
|         uses: docker/setup-qemu-action@v3.6.0 | ||||
|  | ||||
|       - name: Log in to docker hub | ||||
|         uses: docker/login-action@v3.3.0 | ||||
| @@ -140,7 +140,7 @@ jobs: | ||||
|           echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Upload digests | ||||
|         uses: actions/upload-artifact@v4.6.0 | ||||
|         uses: actions/upload-artifact@v4.6.1 | ||||
|         with: | ||||
|           name: digests-${{ steps.sanitize.outputs.name }} | ||||
|           path: /tmp/digests | ||||
| @@ -176,14 +176,14 @@ jobs: | ||||
|       - uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Download digests | ||||
|         uses: actions/download-artifact@v4.1.8 | ||||
|         uses: actions/download-artifact@v4.1.9 | ||||
|         with: | ||||
|           pattern: digests-* | ||||
|           path: /tmp/digests | ||||
|           merge-multiple: true | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.9.0 | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|  | ||||
|       - name: Log in to docker hub | ||||
|         if: matrix.registry == 'dockerhub' | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							| @@ -36,7 +36,7 @@ jobs: | ||||
|           python ./script/sync-device_class.py | ||||
|  | ||||
|       - name: Commit changes | ||||
|         uses: peter-evans/create-pull-request@v7.0.6 | ||||
|         uses: peter-evans/create-pull-request@v7.0.7 | ||||
|         with: | ||||
|           commit-message: "Synchronise Device Classes from Home Assistant" | ||||
|           committer: esphomebot <esphome@nabucasa.com> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.5.4 | ||||
|     rev: v0.9.2 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
| @@ -93,6 +93,7 @@ esphome/components/captive_portal/* @OttoWinter | ||||
| esphome/components/ccs811/* @habbie | ||||
| esphome/components/cd74hc4067/* @asoehlke | ||||
| esphome/components/ch422g/* @clydebarrow @jesterret | ||||
| esphome/components/chsc6x/* @kkosik20 | ||||
| esphome/components/climate/* @esphome/core | ||||
| esphome/components/climate_ir/* @glmnet | ||||
| esphome/components/color_temperature/* @jesserockz | ||||
| @@ -234,6 +235,7 @@ esphome/components/kuntze/* @ssieb | ||||
| esphome/components/lcd_menu/* @numo68 | ||||
| esphome/components/ld2410/* @regevbr @sebcaps | ||||
| esphome/components/ld2420/* @descipher | ||||
| esphome/components/ld2450/* @hareeshmu | ||||
| esphome/components/ledc/* @OttoWinter | ||||
| esphome/components/libretiny/* @kuba2k2 | ||||
| esphome/components/libretiny_pwm/* @kuba2k2 | ||||
| @@ -297,6 +299,7 @@ esphome/components/mopeka_std_check/* @Fabian-Schmidt | ||||
| esphome/components/mpl3115a2/* @kbickar | ||||
| esphome/components/mpu6886/* @fabaff | ||||
| esphome/components/ms8607/* @e28eta | ||||
| esphome/components/msa3xx/* @latonita | ||||
| esphome/components/nau7802/* @cujomalainey | ||||
| esphome/components/network/* @esphome/core | ||||
| esphome/components/nextion/* @edwardtfn @senexcrenshaw | ||||
| @@ -445,6 +448,7 @@ esphome/components/tmp102/* @timsavage | ||||
| esphome/components/tmp1075/* @sybrenstuvel | ||||
| esphome/components/tmp117/* @Azimath | ||||
| esphome/components/tof10120/* @wstrzalka | ||||
| esphome/components/tormatic/* @ti-mo | ||||
| esphome/components/toshiba/* @kbx81 | ||||
| esphome/components/touchscreen/* @jesserockz @nielsnl68 | ||||
| esphome/components/tsl2591/* @wjcarpenter | ||||
|   | ||||
| @@ -23,10 +23,6 @@ if bashio::config.true 'streamer_mode'; then | ||||
|     export ESPHOME_STREAMER_MODE=true | ||||
| fi | ||||
|  | ||||
| if bashio::config.true 'status_use_ping'; then | ||||
|     export ESPHOME_DASHBOARD_USE_PING=true | ||||
| fi | ||||
|  | ||||
| if bashio::config.has_value 'relative_url'; then | ||||
|     export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url') | ||||
| fi | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| import argparse | ||||
| from datetime import datetime | ||||
| import functools | ||||
| import importlib | ||||
| import logging | ||||
| import os | ||||
| import re | ||||
| @@ -66,7 +67,7 @@ def choose_prompt(options, purpose: str = None): | ||||
|         return options[0][1] | ||||
|  | ||||
|     safe_print( | ||||
|         f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:' | ||||
|         f"Found multiple options{f' for {purpose}' if purpose else ''}, please choose one:" | ||||
|     ) | ||||
|     for i, (desc, _) in enumerate(options): | ||||
|         safe_print(f"  [{i + 1}] {desc}") | ||||
| @@ -336,6 +337,13 @@ def check_permissions(port): | ||||
|  | ||||
|  | ||||
| def upload_program(config, args, host): | ||||
|     try: | ||||
|         module = importlib.import_module("esphome.components." + CORE.target_platform) | ||||
|         if getattr(module, "upload_program")(config, args, host): | ||||
|             return 0 | ||||
|     except AttributeError: | ||||
|         pass | ||||
|  | ||||
|     if get_port_type(host) == "SERIAL": | ||||
|         check_permissions(host) | ||||
|         if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): | ||||
|   | ||||
| @@ -227,6 +227,9 @@ message DeviceInfoResponse { | ||||
|   uint32 voice_assistant_feature_flags = 17; | ||||
|  | ||||
|   string suggested_area = 16; | ||||
|  | ||||
|   // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA" | ||||
|   string bluetooth_mac_address = 18; | ||||
| } | ||||
|  | ||||
| message ListEntitiesRequest { | ||||
|   | ||||
| @@ -28,8 +28,38 @@ namespace api { | ||||
| static const char *const TAG = "api.connection"; | ||||
| static const int ESP32_CAMERA_STOP_STREAM = 5000; | ||||
|  | ||||
| // helper for allowing only unique entries in the queue | ||||
| void DeferredMessageQueue::dmq_push_back_with_dedup_(void *source, send_message_t *send_message) { | ||||
|   DeferredMessage item(source, send_message); | ||||
|  | ||||
|   auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(), | ||||
|                            [&item](const DeferredMessage &test) -> bool { return test == item; }); | ||||
|  | ||||
|   if (iter != this->deferred_queue_.end()) { | ||||
|     (*iter) = item; | ||||
|   } else { | ||||
|     this->deferred_queue_.push_back(item); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void DeferredMessageQueue::process_queue() { | ||||
|   while (!deferred_queue_.empty()) { | ||||
|     DeferredMessage &de = deferred_queue_.front(); | ||||
|     if (de.send_message_(this->api_connection_, de.source_)) { | ||||
|       // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen | ||||
|       deferred_queue_.erase(deferred_queue_.begin()); | ||||
|     } else { | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void DeferredMessageQueue::defer(void *source, send_message_t *send_message) { | ||||
|   this->dmq_push_back_with_dedup_(source, send_message); | ||||
| } | ||||
|  | ||||
| APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | ||||
|     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { | ||||
|     : parent_(parent), deferred_message_queue_(this), initial_state_iterator_(this), list_entities_iterator_(this) { | ||||
|   this->proto_write_buffer_.reserve(64); | ||||
|  | ||||
| #if defined(USE_API_PLAINTEXT) | ||||
| @@ -116,7 +146,11 @@ void APIConnection::loop() { | ||||
|       return; | ||||
|   } | ||||
|  | ||||
|   this->deferred_message_queue_.process_queue(); | ||||
|  | ||||
|   if (!this->list_entities_iterator_.completed()) | ||||
|     this->list_entities_iterator_.advance(); | ||||
|   if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed()) | ||||
|     this->initial_state_iterator_.advance(); | ||||
|  | ||||
|   static uint32_t keepalive = 60000; | ||||
| @@ -210,13 +244,31 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_binary_sensor_state(this, binary_sensor, state)) { | ||||
|     this->deferred_message_queue_.defer(binary_sensor, try_send_binary_sensor_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) { | ||||
|   if (!APIConnection::try_send_binary_sensor_info(this, binary_sensor)) { | ||||
|     this->deferred_message_queue_.defer(binary_sensor, try_send_binary_sensor_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor) { | ||||
|   binary_sensor::BinarySensor *binary_sensor = reinterpret_cast<binary_sensor::BinarySensor *>(v_binary_sensor); | ||||
|   return APIConnection::try_send_binary_sensor_state(api, binary_sensor, binary_sensor->state); | ||||
| } | ||||
| bool APIConnection::try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, | ||||
|                                                  bool state) { | ||||
|   BinarySensorStateResponse resp; | ||||
|   resp.key = binary_sensor->get_object_id_hash(); | ||||
|   resp.state = state; | ||||
|   resp.missing_state = !binary_sensor->has_state(); | ||||
|   return this->send_binary_sensor_state_response(resp); | ||||
|   return api->send_binary_sensor_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) { | ||||
| bool APIConnection::try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor) { | ||||
|   binary_sensor::BinarySensor *binary_sensor = reinterpret_cast<binary_sensor::BinarySensor *>(v_binary_sensor); | ||||
|   ListEntitiesBinarySensorResponse msg; | ||||
|   msg.object_id = binary_sensor->get_object_id(); | ||||
|   msg.key = binary_sensor->get_object_id_hash(); | ||||
| @@ -228,7 +280,7 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_ | ||||
|   msg.disabled_by_default = binary_sensor->is_disabled_by_default(); | ||||
|   msg.icon = binary_sensor->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(binary_sensor->get_entity_category()); | ||||
|   return this->send_list_entities_binary_sensor_response(msg); | ||||
|   return api->send_list_entities_binary_sensor_response(msg); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @@ -237,6 +289,19 @@ bool APIConnection::send_cover_state(cover::Cover *cover) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_cover_state(this, cover)) { | ||||
|     this->deferred_message_queue_.defer(cover, try_send_cover_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_cover_info(cover::Cover *cover) { | ||||
|   if (!APIConnection::try_send_cover_info(this, cover)) { | ||||
|     this->deferred_message_queue_.defer(cover, try_send_cover_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_cover_state(APIConnection *api, void *v_cover) { | ||||
|   cover::Cover *cover = reinterpret_cast<cover::Cover *>(v_cover); | ||||
|   auto traits = cover->get_traits(); | ||||
|   CoverStateResponse resp{}; | ||||
|   resp.key = cover->get_object_id_hash(); | ||||
| @@ -246,9 +311,10 @@ bool APIConnection::send_cover_state(cover::Cover *cover) { | ||||
|   if (traits.get_supports_tilt()) | ||||
|     resp.tilt = cover->tilt; | ||||
|   resp.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); | ||||
|   return this->send_cover_state_response(resp); | ||||
|   return api->send_cover_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_cover_info(cover::Cover *cover) { | ||||
| bool APIConnection::try_send_cover_info(APIConnection *api, void *v_cover) { | ||||
|   cover::Cover *cover = reinterpret_cast<cover::Cover *>(v_cover); | ||||
|   auto traits = cover->get_traits(); | ||||
|   ListEntitiesCoverResponse msg; | ||||
|   msg.key = cover->get_object_id_hash(); | ||||
| @@ -264,7 +330,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) { | ||||
|   msg.disabled_by_default = cover->is_disabled_by_default(); | ||||
|   msg.icon = cover->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(cover->get_entity_category()); | ||||
|   return this->send_list_entities_cover_response(msg); | ||||
|   return api->send_list_entities_cover_response(msg); | ||||
| } | ||||
| void APIConnection::cover_command(const CoverCommandRequest &msg) { | ||||
|   cover::Cover *cover = App.get_cover_by_key(msg.key); | ||||
| @@ -300,6 +366,19 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_fan_state(this, fan)) { | ||||
|     this->deferred_message_queue_.defer(fan, try_send_fan_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_fan_info(fan::Fan *fan) { | ||||
|   if (!APIConnection::try_send_fan_info(this, fan)) { | ||||
|     this->deferred_message_queue_.defer(fan, try_send_fan_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_fan_state(APIConnection *api, void *v_fan) { | ||||
|   fan::Fan *fan = reinterpret_cast<fan::Fan *>(v_fan); | ||||
|   auto traits = fan->get_traits(); | ||||
|   FanStateResponse resp{}; | ||||
|   resp.key = fan->get_object_id_hash(); | ||||
| @@ -313,9 +392,10 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { | ||||
|     resp.direction = static_cast<enums::FanDirection>(fan->direction); | ||||
|   if (traits.supports_preset_modes()) | ||||
|     resp.preset_mode = fan->preset_mode; | ||||
|   return this->send_fan_state_response(resp); | ||||
|   return api->send_fan_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_fan_info(fan::Fan *fan) { | ||||
| bool APIConnection::try_send_fan_info(APIConnection *api, void *v_fan) { | ||||
|   fan::Fan *fan = reinterpret_cast<fan::Fan *>(v_fan); | ||||
|   auto traits = fan->get_traits(); | ||||
|   ListEntitiesFanResponse msg; | ||||
|   msg.key = fan->get_object_id_hash(); | ||||
| @@ -332,7 +412,7 @@ bool APIConnection::send_fan_info(fan::Fan *fan) { | ||||
|   msg.disabled_by_default = fan->is_disabled_by_default(); | ||||
|   msg.icon = fan->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category()); | ||||
|   return this->send_list_entities_fan_response(msg); | ||||
|   return api->send_list_entities_fan_response(msg); | ||||
| } | ||||
| void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
|   fan::Fan *fan = App.get_fan_by_key(msg.key); | ||||
| @@ -361,6 +441,19 @@ bool APIConnection::send_light_state(light::LightState *light) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_light_state(this, light)) { | ||||
|     this->deferred_message_queue_.defer(light, try_send_light_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_light_info(light::LightState *light) { | ||||
|   if (!APIConnection::try_send_light_info(this, light)) { | ||||
|     this->deferred_message_queue_.defer(light, try_send_light_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_light_state(APIConnection *api, void *v_light) { | ||||
|   light::LightState *light = reinterpret_cast<light::LightState *>(v_light); | ||||
|   auto traits = light->get_traits(); | ||||
|   auto values = light->remote_values; | ||||
|   auto color_mode = values.get_color_mode(); | ||||
| @@ -380,9 +473,10 @@ bool APIConnection::send_light_state(light::LightState *light) { | ||||
|   resp.warm_white = values.get_warm_white(); | ||||
|   if (light->supports_effects()) | ||||
|     resp.effect = light->get_effect_name(); | ||||
|   return this->send_light_state_response(resp); | ||||
|   return api->send_light_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_light_info(light::LightState *light) { | ||||
| bool APIConnection::try_send_light_info(APIConnection *api, void *v_light) { | ||||
|   light::LightState *light = reinterpret_cast<light::LightState *>(v_light); | ||||
|   auto traits = light->get_traits(); | ||||
|   ListEntitiesLightResponse msg; | ||||
|   msg.key = light->get_object_id_hash(); | ||||
| @@ -415,7 +509,7 @@ bool APIConnection::send_light_info(light::LightState *light) { | ||||
|     for (auto *effect : light->get_effects()) | ||||
|       msg.effects.push_back(effect->get_name()); | ||||
|   } | ||||
|   return this->send_list_entities_light_response(msg); | ||||
|   return api->send_list_entities_light_response(msg); | ||||
| } | ||||
| void APIConnection::light_command(const LightCommandRequest &msg) { | ||||
|   light::LightState *light = App.get_light_by_key(msg.key); | ||||
| @@ -459,13 +553,30 @@ bool APIConnection::send_sensor_state(sensor::Sensor *sensor, float state) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_sensor_state(this, sensor, state)) { | ||||
|     this->deferred_message_queue_.defer(sensor, try_send_sensor_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_sensor_info(sensor::Sensor *sensor) { | ||||
|   if (!APIConnection::try_send_sensor_info(this, sensor)) { | ||||
|     this->deferred_message_queue_.defer(sensor, try_send_sensor_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_sensor_state(APIConnection *api, void *v_sensor) { | ||||
|   sensor::Sensor *sensor = reinterpret_cast<sensor::Sensor *>(v_sensor); | ||||
|   return APIConnection::try_send_sensor_state(api, sensor, sensor->state); | ||||
| } | ||||
| bool APIConnection::try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state) { | ||||
|   SensorStateResponse resp{}; | ||||
|   resp.key = sensor->get_object_id_hash(); | ||||
|   resp.state = state; | ||||
|   resp.missing_state = !sensor->has_state(); | ||||
|   return this->send_sensor_state_response(resp); | ||||
|   return api->send_sensor_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { | ||||
| bool APIConnection::try_send_sensor_info(APIConnection *api, void *v_sensor) { | ||||
|   sensor::Sensor *sensor = reinterpret_cast<sensor::Sensor *>(v_sensor); | ||||
|   ListEntitiesSensorResponse msg; | ||||
|   msg.key = sensor->get_object_id_hash(); | ||||
|   msg.object_id = sensor->get_object_id(); | ||||
| @@ -482,7 +593,7 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { | ||||
|   msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); | ||||
|   msg.disabled_by_default = sensor->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(sensor->get_entity_category()); | ||||
|   return this->send_list_entities_sensor_response(msg); | ||||
|   return api->send_list_entities_sensor_response(msg); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @@ -491,12 +602,29 @@ bool APIConnection::send_switch_state(switch_::Switch *a_switch, bool state) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_switch_state(this, a_switch, state)) { | ||||
|     this->deferred_message_queue_.defer(a_switch, try_send_switch_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_switch_info(switch_::Switch *a_switch) { | ||||
|   if (!APIConnection::try_send_switch_info(this, a_switch)) { | ||||
|     this->deferred_message_queue_.defer(a_switch, try_send_switch_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_switch_state(APIConnection *api, void *v_a_switch) { | ||||
|   switch_::Switch *a_switch = reinterpret_cast<switch_::Switch *>(v_a_switch); | ||||
|   return APIConnection::try_send_switch_state(api, a_switch, a_switch->state); | ||||
| } | ||||
| bool APIConnection::try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state) { | ||||
|   SwitchStateResponse resp{}; | ||||
|   resp.key = a_switch->get_object_id_hash(); | ||||
|   resp.state = state; | ||||
|   return this->send_switch_state_response(resp); | ||||
|   return api->send_switch_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_switch_info(switch_::Switch *a_switch) { | ||||
| bool APIConnection::try_send_switch_info(APIConnection *api, void *v_a_switch) { | ||||
|   switch_::Switch *a_switch = reinterpret_cast<switch_::Switch *>(v_a_switch); | ||||
|   ListEntitiesSwitchResponse msg; | ||||
|   msg.key = a_switch->get_object_id_hash(); | ||||
|   msg.object_id = a_switch->get_object_id(); | ||||
| @@ -508,7 +636,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) { | ||||
|   msg.disabled_by_default = a_switch->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(a_switch->get_entity_category()); | ||||
|   msg.device_class = a_switch->get_device_class(); | ||||
|   return this->send_list_entities_switch_response(msg); | ||||
|   return api->send_list_entities_switch_response(msg); | ||||
| } | ||||
| void APIConnection::switch_command(const SwitchCommandRequest &msg) { | ||||
|   switch_::Switch *a_switch = App.get_switch_by_key(msg.key); | ||||
| @@ -528,13 +656,31 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor, | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_text_sensor_state(this, text_sensor, std::move(state))) { | ||||
|     this->deferred_message_queue_.defer(text_sensor, try_send_text_sensor_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) { | ||||
|   if (!APIConnection::try_send_text_sensor_info(this, text_sensor)) { | ||||
|     this->deferred_message_queue_.defer(text_sensor, try_send_text_sensor_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_text_sensor_state(APIConnection *api, void *v_text_sensor) { | ||||
|   text_sensor::TextSensor *text_sensor = reinterpret_cast<text_sensor::TextSensor *>(v_text_sensor); | ||||
|   return APIConnection::try_send_text_sensor_state(api, text_sensor, text_sensor->state); | ||||
| } | ||||
| bool APIConnection::try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, | ||||
|                                                std::string state) { | ||||
|   TextSensorStateResponse resp{}; | ||||
|   resp.key = text_sensor->get_object_id_hash(); | ||||
|   resp.state = std::move(state); | ||||
|   resp.missing_state = !text_sensor->has_state(); | ||||
|   return this->send_text_sensor_state_response(resp); | ||||
|   return api->send_text_sensor_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) { | ||||
| bool APIConnection::try_send_text_sensor_info(APIConnection *api, void *v_text_sensor) { | ||||
|   text_sensor::TextSensor *text_sensor = reinterpret_cast<text_sensor::TextSensor *>(v_text_sensor); | ||||
|   ListEntitiesTextSensorResponse msg; | ||||
|   msg.key = text_sensor->get_object_id_hash(); | ||||
|   msg.object_id = text_sensor->get_object_id(); | ||||
| @@ -546,7 +692,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) | ||||
|   msg.disabled_by_default = text_sensor->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(text_sensor->get_entity_category()); | ||||
|   msg.device_class = text_sensor->get_device_class(); | ||||
|   return this->send_list_entities_text_sensor_response(msg); | ||||
|   return api->send_list_entities_text_sensor_response(msg); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @@ -555,6 +701,19 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_climate_state(this, climate)) { | ||||
|     this->deferred_message_queue_.defer(climate, try_send_climate_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_climate_info(climate::Climate *climate) { | ||||
|   if (!APIConnection::try_send_climate_info(this, climate)) { | ||||
|     this->deferred_message_queue_.defer(climate, try_send_climate_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_climate_state(APIConnection *api, void *v_climate) { | ||||
|   climate::Climate *climate = reinterpret_cast<climate::Climate *>(v_climate); | ||||
|   auto traits = climate->get_traits(); | ||||
|   ClimateStateResponse resp{}; | ||||
|   resp.key = climate->get_object_id_hash(); | ||||
| @@ -583,9 +742,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { | ||||
|     resp.current_humidity = climate->current_humidity; | ||||
|   if (traits.get_supports_target_humidity()) | ||||
|     resp.target_humidity = climate->target_humidity; | ||||
|   return this->send_climate_state_response(resp); | ||||
|   return api->send_climate_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_climate_info(climate::Climate *climate) { | ||||
| bool APIConnection::try_send_climate_info(APIConnection *api, void *v_climate) { | ||||
|   climate::Climate *climate = reinterpret_cast<climate::Climate *>(v_climate); | ||||
|   auto traits = climate->get_traits(); | ||||
|   ListEntitiesClimateResponse msg; | ||||
|   msg.key = climate->get_object_id_hash(); | ||||
| @@ -626,7 +786,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { | ||||
|     msg.supported_custom_presets.push_back(custom_preset); | ||||
|   for (auto swing_mode : traits.get_supported_swing_modes()) | ||||
|     msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode)); | ||||
|   return this->send_list_entities_climate_response(msg); | ||||
|   return api->send_list_entities_climate_response(msg); | ||||
| } | ||||
| void APIConnection::climate_command(const ClimateCommandRequest &msg) { | ||||
|   climate::Climate *climate = App.get_climate_by_key(msg.key); | ||||
| @@ -663,13 +823,30 @@ bool APIConnection::send_number_state(number::Number *number, float state) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_number_state(this, number, state)) { | ||||
|     this->deferred_message_queue_.defer(number, try_send_number_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_number_info(number::Number *number) { | ||||
|   if (!APIConnection::try_send_number_info(this, number)) { | ||||
|     this->deferred_message_queue_.defer(number, try_send_number_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_number_state(APIConnection *api, void *v_number) { | ||||
|   number::Number *number = reinterpret_cast<number::Number *>(v_number); | ||||
|   return APIConnection::try_send_number_state(api, number, number->state); | ||||
| } | ||||
| bool APIConnection::try_send_number_state(APIConnection *api, number::Number *number, float state) { | ||||
|   NumberStateResponse resp{}; | ||||
|   resp.key = number->get_object_id_hash(); | ||||
|   resp.state = state; | ||||
|   resp.missing_state = !number->has_state(); | ||||
|   return this->send_number_state_response(resp); | ||||
|   return api->send_number_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_number_info(number::Number *number) { | ||||
| bool APIConnection::try_send_number_info(APIConnection *api, void *v_number) { | ||||
|   number::Number *number = reinterpret_cast<number::Number *>(v_number); | ||||
|   ListEntitiesNumberResponse msg; | ||||
|   msg.key = number->get_object_id_hash(); | ||||
|   msg.object_id = number->get_object_id(); | ||||
| @@ -687,7 +864,7 @@ bool APIConnection::send_number_info(number::Number *number) { | ||||
|   msg.max_value = number->traits.get_max_value(); | ||||
|   msg.step = number->traits.get_step(); | ||||
|  | ||||
|   return this->send_list_entities_number_response(msg); | ||||
|   return api->send_list_entities_number_response(msg); | ||||
| } | ||||
| void APIConnection::number_command(const NumberCommandRequest &msg) { | ||||
|   number::Number *number = App.get_number_by_key(msg.key); | ||||
| @@ -705,15 +882,29 @@ bool APIConnection::send_date_state(datetime::DateEntity *date) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_date_state(this, date)) { | ||||
|     this->deferred_message_queue_.defer(date, try_send_date_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_date_info(datetime::DateEntity *date) { | ||||
|   if (!APIConnection::try_send_date_info(this, date)) { | ||||
|     this->deferred_message_queue_.defer(date, try_send_date_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_date_state(APIConnection *api, void *v_date) { | ||||
|   datetime::DateEntity *date = reinterpret_cast<datetime::DateEntity *>(v_date); | ||||
|   DateStateResponse resp{}; | ||||
|   resp.key = date->get_object_id_hash(); | ||||
|   resp.missing_state = !date->has_state(); | ||||
|   resp.year = date->year; | ||||
|   resp.month = date->month; | ||||
|   resp.day = date->day; | ||||
|   return this->send_date_state_response(resp); | ||||
|   return api->send_date_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_date_info(datetime::DateEntity *date) { | ||||
| bool APIConnection::try_send_date_info(APIConnection *api, void *v_date) { | ||||
|   datetime::DateEntity *date = reinterpret_cast<datetime::DateEntity *>(v_date); | ||||
|   ListEntitiesDateResponse msg; | ||||
|   msg.key = date->get_object_id_hash(); | ||||
|   msg.object_id = date->get_object_id(); | ||||
| @@ -724,7 +915,7 @@ bool APIConnection::send_date_info(datetime::DateEntity *date) { | ||||
|   msg.disabled_by_default = date->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(date->get_entity_category()); | ||||
|  | ||||
|   return this->send_list_entities_date_response(msg); | ||||
|   return api->send_list_entities_date_response(msg); | ||||
| } | ||||
| void APIConnection::date_command(const DateCommandRequest &msg) { | ||||
|   datetime::DateEntity *date = App.get_date_by_key(msg.key); | ||||
| @@ -742,15 +933,29 @@ bool APIConnection::send_time_state(datetime::TimeEntity *time) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_time_state(this, time)) { | ||||
|     this->deferred_message_queue_.defer(time, try_send_time_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_time_info(datetime::TimeEntity *time) { | ||||
|   if (!APIConnection::try_send_time_info(this, time)) { | ||||
|     this->deferred_message_queue_.defer(time, try_send_time_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_time_state(APIConnection *api, void *v_time) { | ||||
|   datetime::TimeEntity *time = reinterpret_cast<datetime::TimeEntity *>(v_time); | ||||
|   TimeStateResponse resp{}; | ||||
|   resp.key = time->get_object_id_hash(); | ||||
|   resp.missing_state = !time->has_state(); | ||||
|   resp.hour = time->hour; | ||||
|   resp.minute = time->minute; | ||||
|   resp.second = time->second; | ||||
|   return this->send_time_state_response(resp); | ||||
|   return api->send_time_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_time_info(datetime::TimeEntity *time) { | ||||
| bool APIConnection::try_send_time_info(APIConnection *api, void *v_time) { | ||||
|   datetime::TimeEntity *time = reinterpret_cast<datetime::TimeEntity *>(v_time); | ||||
|   ListEntitiesTimeResponse msg; | ||||
|   msg.key = time->get_object_id_hash(); | ||||
|   msg.object_id = time->get_object_id(); | ||||
| @@ -761,7 +966,7 @@ bool APIConnection::send_time_info(datetime::TimeEntity *time) { | ||||
|   msg.disabled_by_default = time->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(time->get_entity_category()); | ||||
|  | ||||
|   return this->send_list_entities_time_response(msg); | ||||
|   return api->send_list_entities_time_response(msg); | ||||
| } | ||||
| void APIConnection::time_command(const TimeCommandRequest &msg) { | ||||
|   datetime::TimeEntity *time = App.get_time_by_key(msg.key); | ||||
| @@ -779,6 +984,19 @@ bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_datetime_state(this, datetime)) { | ||||
|     this->deferred_message_queue_.defer(datetime, try_send_datetime_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { | ||||
|   if (!APIConnection::try_send_datetime_info(this, datetime)) { | ||||
|     this->deferred_message_queue_.defer(datetime, try_send_datetime_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_datetime_state(APIConnection *api, void *v_datetime) { | ||||
|   datetime::DateTimeEntity *datetime = reinterpret_cast<datetime::DateTimeEntity *>(v_datetime); | ||||
|   DateTimeStateResponse resp{}; | ||||
|   resp.key = datetime->get_object_id_hash(); | ||||
|   resp.missing_state = !datetime->has_state(); | ||||
| @@ -786,9 +1004,10 @@ bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { | ||||
|     ESPTime state = datetime->state_as_esptime(); | ||||
|     resp.epoch_seconds = state.timestamp; | ||||
|   } | ||||
|   return this->send_date_time_state_response(resp); | ||||
|   return api->send_date_time_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { | ||||
| bool APIConnection::try_send_datetime_info(APIConnection *api, void *v_datetime) { | ||||
|   datetime::DateTimeEntity *datetime = reinterpret_cast<datetime::DateTimeEntity *>(v_datetime); | ||||
|   ListEntitiesDateTimeResponse msg; | ||||
|   msg.key = datetime->get_object_id_hash(); | ||||
|   msg.object_id = datetime->get_object_id(); | ||||
| @@ -799,7 +1018,7 @@ bool APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { | ||||
|   msg.disabled_by_default = datetime->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(datetime->get_entity_category()); | ||||
|  | ||||
|   return this->send_list_entities_date_time_response(msg); | ||||
|   return api->send_list_entities_date_time_response(msg); | ||||
| } | ||||
| void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { | ||||
|   datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key); | ||||
| @@ -817,13 +1036,30 @@ bool APIConnection::send_text_state(text::Text *text, std::string state) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_text_state(this, text, std::move(state))) { | ||||
|     this->deferred_message_queue_.defer(text, try_send_text_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_text_info(text::Text *text) { | ||||
|   if (!APIConnection::try_send_text_info(this, text)) { | ||||
|     this->deferred_message_queue_.defer(text, try_send_text_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_text_state(APIConnection *api, void *v_text) { | ||||
|   text::Text *text = reinterpret_cast<text::Text *>(v_text); | ||||
|   return APIConnection::try_send_text_state(api, text, text->state); | ||||
| } | ||||
| bool APIConnection::try_send_text_state(APIConnection *api, text::Text *text, std::string state) { | ||||
|   TextStateResponse resp{}; | ||||
|   resp.key = text->get_object_id_hash(); | ||||
|   resp.state = std::move(state); | ||||
|   resp.missing_state = !text->has_state(); | ||||
|   return this->send_text_state_response(resp); | ||||
|   return api->send_text_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_text_info(text::Text *text) { | ||||
| bool APIConnection::try_send_text_info(APIConnection *api, void *v_text) { | ||||
|   text::Text *text = reinterpret_cast<text::Text *>(v_text); | ||||
|   ListEntitiesTextResponse msg; | ||||
|   msg.key = text->get_object_id_hash(); | ||||
|   msg.object_id = text->get_object_id(); | ||||
| @@ -837,7 +1073,7 @@ bool APIConnection::send_text_info(text::Text *text) { | ||||
|   msg.max_length = text->traits.get_max_length(); | ||||
|   msg.pattern = text->traits.get_pattern(); | ||||
|  | ||||
|   return this->send_list_entities_text_response(msg); | ||||
|   return api->send_list_entities_text_response(msg); | ||||
| } | ||||
| void APIConnection::text_command(const TextCommandRequest &msg) { | ||||
|   text::Text *text = App.get_text_by_key(msg.key); | ||||
| @@ -855,13 +1091,30 @@ bool APIConnection::send_select_state(select::Select *select, std::string state) | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_select_state(this, select, std::move(state))) { | ||||
|     this->deferred_message_queue_.defer(select, try_send_select_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_select_info(select::Select *select) { | ||||
|   if (!APIConnection::try_send_select_info(this, select)) { | ||||
|     this->deferred_message_queue_.defer(select, try_send_select_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_select_state(APIConnection *api, void *v_select) { | ||||
|   select::Select *select = reinterpret_cast<select::Select *>(v_select); | ||||
|   return APIConnection::try_send_select_state(api, select, select->state); | ||||
| } | ||||
| bool APIConnection::try_send_select_state(APIConnection *api, select::Select *select, std::string state) { | ||||
|   SelectStateResponse resp{}; | ||||
|   resp.key = select->get_object_id_hash(); | ||||
|   resp.state = std::move(state); | ||||
|   resp.missing_state = !select->has_state(); | ||||
|   return this->send_select_state_response(resp); | ||||
|   return api->send_select_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_select_info(select::Select *select) { | ||||
| bool APIConnection::try_send_select_info(APIConnection *api, void *v_select) { | ||||
|   select::Select *select = reinterpret_cast<select::Select *>(v_select); | ||||
|   ListEntitiesSelectResponse msg; | ||||
|   msg.key = select->get_object_id_hash(); | ||||
|   msg.object_id = select->get_object_id(); | ||||
| @@ -875,7 +1128,7 @@ bool APIConnection::send_select_info(select::Select *select) { | ||||
|   for (const auto &option : select->traits.get_options()) | ||||
|     msg.options.push_back(option); | ||||
|  | ||||
|   return this->send_list_entities_select_response(msg); | ||||
|   return api->send_list_entities_select_response(msg); | ||||
| } | ||||
| void APIConnection::select_command(const SelectCommandRequest &msg) { | ||||
|   select::Select *select = App.get_select_by_key(msg.key); | ||||
| @@ -889,7 +1142,13 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BUTTON | ||||
| bool APIConnection::send_button_info(button::Button *button) { | ||||
| void APIConnection::send_button_info(button::Button *button) { | ||||
|   if (!APIConnection::try_send_button_info(this, button)) { | ||||
|     this->deferred_message_queue_.defer(button, try_send_button_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_button_info(APIConnection *api, void *v_button) { | ||||
|   button::Button *button = reinterpret_cast<button::Button *>(v_button); | ||||
|   ListEntitiesButtonResponse msg; | ||||
|   msg.key = button->get_object_id_hash(); | ||||
|   msg.object_id = button->get_object_id(); | ||||
| @@ -900,7 +1159,7 @@ bool APIConnection::send_button_info(button::Button *button) { | ||||
|   msg.disabled_by_default = button->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(button->get_entity_category()); | ||||
|   msg.device_class = button->get_device_class(); | ||||
|   return this->send_list_entities_button_response(msg); | ||||
|   return api->send_list_entities_button_response(msg); | ||||
| } | ||||
| void APIConnection::button_command(const ButtonCommandRequest &msg) { | ||||
|   button::Button *button = App.get_button_by_key(msg.key); | ||||
| @@ -916,12 +1175,29 @@ bool APIConnection::send_lock_state(lock::Lock *a_lock, lock::LockState state) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_lock_state(this, a_lock, state)) { | ||||
|     this->deferred_message_queue_.defer(a_lock, try_send_lock_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_lock_info(lock::Lock *a_lock) { | ||||
|   if (!APIConnection::try_send_lock_info(this, a_lock)) { | ||||
|     this->deferred_message_queue_.defer(a_lock, try_send_lock_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_lock_state(APIConnection *api, void *v_a_lock) { | ||||
|   lock::Lock *a_lock = reinterpret_cast<lock::Lock *>(v_a_lock); | ||||
|   return APIConnection::try_send_lock_state(api, a_lock, a_lock->state); | ||||
| } | ||||
| bool APIConnection::try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state) { | ||||
|   LockStateResponse resp{}; | ||||
|   resp.key = a_lock->get_object_id_hash(); | ||||
|   resp.state = static_cast<enums::LockState>(state); | ||||
|   return this->send_lock_state_response(resp); | ||||
|   return api->send_lock_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_lock_info(lock::Lock *a_lock) { | ||||
| bool APIConnection::try_send_lock_info(APIConnection *api, void *v_a_lock) { | ||||
|   lock::Lock *a_lock = reinterpret_cast<lock::Lock *>(v_a_lock); | ||||
|   ListEntitiesLockResponse msg; | ||||
|   msg.key = a_lock->get_object_id_hash(); | ||||
|   msg.object_id = a_lock->get_object_id(); | ||||
| @@ -934,7 +1210,7 @@ bool APIConnection::send_lock_info(lock::Lock *a_lock) { | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(a_lock->get_entity_category()); | ||||
|   msg.supports_open = a_lock->traits.get_supports_open(); | ||||
|   msg.requires_code = a_lock->traits.get_requires_code(); | ||||
|   return this->send_list_entities_lock_response(msg); | ||||
|   return api->send_list_entities_lock_response(msg); | ||||
| } | ||||
| void APIConnection::lock_command(const LockCommandRequest &msg) { | ||||
|   lock::Lock *a_lock = App.get_lock_by_key(msg.key); | ||||
| @@ -960,13 +1236,27 @@ bool APIConnection::send_valve_state(valve::Valve *valve) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_valve_state(this, valve)) { | ||||
|     this->deferred_message_queue_.defer(valve, try_send_valve_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_valve_info(valve::Valve *valve) { | ||||
|   if (!APIConnection::try_send_valve_info(this, valve)) { | ||||
|     this->deferred_message_queue_.defer(valve, try_send_valve_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_valve_state(APIConnection *api, void *v_valve) { | ||||
|   valve::Valve *valve = reinterpret_cast<valve::Valve *>(v_valve); | ||||
|   ValveStateResponse resp{}; | ||||
|   resp.key = valve->get_object_id_hash(); | ||||
|   resp.position = valve->position; | ||||
|   resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); | ||||
|   return this->send_valve_state_response(resp); | ||||
|   return api->send_valve_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_valve_info(valve::Valve *valve) { | ||||
| bool APIConnection::try_send_valve_info(APIConnection *api, void *v_valve) { | ||||
|   valve::Valve *valve = reinterpret_cast<valve::Valve *>(v_valve); | ||||
|   auto traits = valve->get_traits(); | ||||
|   ListEntitiesValveResponse msg; | ||||
|   msg.key = valve->get_object_id_hash(); | ||||
| @@ -981,7 +1271,7 @@ bool APIConnection::send_valve_info(valve::Valve *valve) { | ||||
|   msg.assumed_state = traits.get_is_assumed_state(); | ||||
|   msg.supports_position = traits.get_supports_position(); | ||||
|   msg.supports_stop = traits.get_supports_stop(); | ||||
|   return this->send_list_entities_valve_response(msg); | ||||
|   return api->send_list_entities_valve_response(msg); | ||||
| } | ||||
| void APIConnection::valve_command(const ValveCommandRequest &msg) { | ||||
|   valve::Valve *valve = App.get_valve_by_key(msg.key); | ||||
| @@ -1002,6 +1292,19 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_media_player_state(this, media_player)) { | ||||
|     this->deferred_message_queue_.defer(media_player, try_send_media_player_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { | ||||
|   if (!APIConnection::try_send_media_player_info(this, media_player)) { | ||||
|     this->deferred_message_queue_.defer(media_player, try_send_media_player_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_media_player_state(APIConnection *api, void *v_media_player) { | ||||
|   media_player::MediaPlayer *media_player = reinterpret_cast<media_player::MediaPlayer *>(v_media_player); | ||||
|   MediaPlayerStateResponse resp{}; | ||||
|   resp.key = media_player->get_object_id_hash(); | ||||
|  | ||||
| @@ -1011,9 +1314,10 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla | ||||
|   resp.state = static_cast<enums::MediaPlayerState>(report_state); | ||||
|   resp.volume = media_player->volume; | ||||
|   resp.muted = media_player->is_muted(); | ||||
|   return this->send_media_player_state_response(resp); | ||||
|   return api->send_media_player_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { | ||||
| bool APIConnection::try_send_media_player_info(APIConnection *api, void *v_media_player) { | ||||
|   media_player::MediaPlayer *media_player = reinterpret_cast<media_player::MediaPlayer *>(v_media_player); | ||||
|   ListEntitiesMediaPlayerResponse msg; | ||||
|   msg.key = media_player->get_object_id_hash(); | ||||
|   msg.object_id = media_player->get_object_id(); | ||||
| @@ -1037,7 +1341,7 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play | ||||
|     msg.supported_formats.push_back(media_format); | ||||
|   } | ||||
|  | ||||
|   return this->send_list_entities_media_player_response(msg); | ||||
|   return api->send_list_entities_media_player_response(msg); | ||||
| } | ||||
| void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { | ||||
|   media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); | ||||
| @@ -1062,7 +1366,7 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) { | ||||
| void APIConnection::set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) { | ||||
|   if (!this->state_subscription_) | ||||
|     return; | ||||
|   if (this->image_reader_.available()) | ||||
| @@ -1071,7 +1375,13 @@ void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> | ||||
|       image->was_requested_by(esphome::esp32_camera::IDLE)) | ||||
|     this->image_reader_.set_image(std::move(image)); | ||||
| } | ||||
| bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { | ||||
| void APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { | ||||
|   if (!APIConnection::try_send_camera_info(this, camera)) { | ||||
|     this->deferred_message_queue_.defer(camera, try_send_camera_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_camera_info(APIConnection *api, void *v_camera) { | ||||
|   esp32_camera::ESP32Camera *camera = reinterpret_cast<esp32_camera::ESP32Camera *>(v_camera); | ||||
|   ListEntitiesCameraResponse msg; | ||||
|   msg.key = camera->get_object_id_hash(); | ||||
|   msg.object_id = camera->get_object_id(); | ||||
| @@ -1081,7 +1391,7 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { | ||||
|   msg.disabled_by_default = camera->is_disabled_by_default(); | ||||
|   msg.icon = camera->get_icon(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(camera->get_entity_category()); | ||||
|   return this->send_list_entities_camera_response(msg); | ||||
|   return api->send_list_entities_camera_response(msg); | ||||
| } | ||||
| void APIConnection::camera_image(const CameraImageRequest &msg) { | ||||
|   if (esp32_camera::global_esp32_camera == nullptr) | ||||
| @@ -1268,12 +1578,28 @@ bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmCon | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_alarm_control_panel_state(this, a_alarm_control_panel)) { | ||||
|     this->deferred_message_queue_.defer(a_alarm_control_panel, try_send_alarm_control_panel_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { | ||||
|   if (!APIConnection::try_send_alarm_control_panel_info(this, a_alarm_control_panel)) { | ||||
|     this->deferred_message_queue_.defer(a_alarm_control_panel, try_send_alarm_control_panel_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel) { | ||||
|   alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = | ||||
|       reinterpret_cast<alarm_control_panel::AlarmControlPanel *>(v_a_alarm_control_panel); | ||||
|   AlarmControlPanelStateResponse resp{}; | ||||
|   resp.key = a_alarm_control_panel->get_object_id_hash(); | ||||
|   resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); | ||||
|   return this->send_alarm_control_panel_state_response(resp); | ||||
|   return api->send_alarm_control_panel_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { | ||||
| bool APIConnection::try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel) { | ||||
|   alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = | ||||
|       reinterpret_cast<alarm_control_panel::AlarmControlPanel *>(v_a_alarm_control_panel); | ||||
|   ListEntitiesAlarmControlPanelResponse msg; | ||||
|   msg.key = a_alarm_control_panel->get_object_id_hash(); | ||||
|   msg.object_id = a_alarm_control_panel->get_object_id(); | ||||
| @@ -1285,7 +1611,7 @@ bool APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmCont | ||||
|   msg.supported_features = a_alarm_control_panel->get_supported_features(); | ||||
|   msg.requires_code = a_alarm_control_panel->get_requires_code(); | ||||
|   msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); | ||||
|   return this->send_list_entities_alarm_control_panel_response(msg); | ||||
|   return api->send_list_entities_alarm_control_panel_response(msg); | ||||
| } | ||||
| void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { | ||||
|   alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key); | ||||
| @@ -1322,13 +1648,28 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_EVENT | ||||
| bool APIConnection::send_event(event::Event *event, std::string event_type) { | ||||
| void APIConnection::send_event(event::Event *event, std::string event_type) { | ||||
|   if (!APIConnection::try_send_event(this, event, std::move(event_type))) { | ||||
|     this->deferred_message_queue_.defer(event, try_send_event); | ||||
|   } | ||||
| } | ||||
| void APIConnection::send_event_info(event::Event *event) { | ||||
|   if (!APIConnection::try_send_event_info(this, event)) { | ||||
|     this->deferred_message_queue_.defer(event, try_send_event_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_event(APIConnection *api, void *v_event) { | ||||
|   event::Event *event = reinterpret_cast<event::Event *>(v_event); | ||||
|   return APIConnection::try_send_event(api, event, *(event->last_event_type)); | ||||
| } | ||||
| bool APIConnection::try_send_event(APIConnection *api, event::Event *event, std::string event_type) { | ||||
|   EventResponse resp{}; | ||||
|   resp.key = event->get_object_id_hash(); | ||||
|   resp.event_type = std::move(event_type); | ||||
|   return this->send_event_response(resp); | ||||
|   return api->send_event_response(resp); | ||||
| } | ||||
| bool APIConnection::send_event_info(event::Event *event) { | ||||
| bool APIConnection::try_send_event_info(APIConnection *api, void *v_event) { | ||||
|   event::Event *event = reinterpret_cast<event::Event *>(v_event); | ||||
|   ListEntitiesEventResponse msg; | ||||
|   msg.key = event->get_object_id_hash(); | ||||
|   msg.object_id = event->get_object_id(); | ||||
| @@ -1341,7 +1682,7 @@ bool APIConnection::send_event_info(event::Event *event) { | ||||
|   msg.device_class = event->get_device_class(); | ||||
|   for (const auto &event_type : event->get_event_types()) | ||||
|     msg.event_types.push_back(event_type); | ||||
|   return this->send_list_entities_event_response(msg); | ||||
|   return api->send_list_entities_event_response(msg); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @@ -1350,6 +1691,19 @@ bool APIConnection::send_update_state(update::UpdateEntity *update) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   if (!APIConnection::try_send_update_state(this, update)) { | ||||
|     this->deferred_message_queue_.defer(update, try_send_update_state); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::send_update_info(update::UpdateEntity *update) { | ||||
|   if (!APIConnection::try_send_update_info(this, update)) { | ||||
|     this->deferred_message_queue_.defer(update, try_send_update_info); | ||||
|   } | ||||
| } | ||||
| bool APIConnection::try_send_update_state(APIConnection *api, void *v_update) { | ||||
|   update::UpdateEntity *update = reinterpret_cast<update::UpdateEntity *>(v_update); | ||||
|   UpdateStateResponse resp{}; | ||||
|   resp.key = update->get_object_id_hash(); | ||||
|   resp.missing_state = !update->has_state(); | ||||
| @@ -1366,9 +1720,10 @@ bool APIConnection::send_update_state(update::UpdateEntity *update) { | ||||
|     resp.release_url = update->update_info.release_url; | ||||
|   } | ||||
|  | ||||
|   return this->send_update_state_response(resp); | ||||
|   return api->send_update_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_update_info(update::UpdateEntity *update) { | ||||
| bool APIConnection::try_send_update_info(APIConnection *api, void *v_update) { | ||||
|   update::UpdateEntity *update = reinterpret_cast<update::UpdateEntity *>(v_update); | ||||
|   ListEntitiesUpdateResponse msg; | ||||
|   msg.key = update->get_object_id_hash(); | ||||
|   msg.object_id = update->get_object_id(); | ||||
| @@ -1379,7 +1734,7 @@ bool APIConnection::send_update_info(update::UpdateEntity *update) { | ||||
|   msg.disabled_by_default = update->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(update->get_entity_category()); | ||||
|   msg.device_class = update->get_device_class(); | ||||
|   return this->send_list_entities_update_response(msg); | ||||
|   return api->send_list_entities_update_response(msg); | ||||
| } | ||||
| void APIConnection::update_command(const UpdateCommandRequest &msg) { | ||||
|   update::UpdateEntity *update = App.get_update_by_key(msg.key); | ||||
| @@ -1403,7 +1758,7 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| bool APIConnection::send_log_message(int level, const char *tag, const char *line) { | ||||
| bool APIConnection::try_send_log_message(int level, const char *tag, const char *line) { | ||||
|   if (this->log_subscription_ < level) | ||||
|     return false; | ||||
|  | ||||
| @@ -1488,6 +1843,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version(); | ||||
|   resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); | ||||
|   resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version(); | ||||
|   | ||||
| @@ -14,6 +14,46 @@ | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| using send_message_t = bool(APIConnection *, void *); | ||||
|  | ||||
| /* | ||||
|   This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that | ||||
|   will lazily publish that message.  The two pointers allow dedup in the deferred queue if multiple publishes for the | ||||
|   same component are backed up, and take up only 8 bytes of memory.  The entry in the deferred queue (a std::vector) is | ||||
|   the DeferredMessage instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per entry.  Even | ||||
|   100 backed up messages (you'd have to have at least 100 sensors publishing because of dedup) would take up only 0.8 | ||||
|   kB. | ||||
| */ | ||||
| class DeferredMessageQueue { | ||||
|   struct DeferredMessage { | ||||
|     friend class DeferredMessageQueue; | ||||
|  | ||||
|    protected: | ||||
|     void *source_; | ||||
|     send_message_t *send_message_; | ||||
|  | ||||
|    public: | ||||
|     DeferredMessage(void *source, send_message_t *send_message) : source_(source), send_message_(send_message) {} | ||||
|     bool operator==(const DeferredMessage &test) const { | ||||
|       return (source_ == test.source_ && send_message_ == test.send_message_); | ||||
|     } | ||||
|   } __attribute__((packed)); | ||||
|  | ||||
|  protected: | ||||
|   // vector is used very specifically for its zero memory overhead even though items are popped from the front (memory | ||||
|   // footprint is more important than speed here) | ||||
|   std::vector<DeferredMessage> deferred_queue_; | ||||
|   APIConnection *api_connection_; | ||||
|  | ||||
|   // helper for allowing only unique entries in the queue | ||||
|   void dmq_push_back_with_dedup_(void *source, send_message_t *send_message); | ||||
|  | ||||
|  public: | ||||
|   DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {} | ||||
|   void process_queue(); | ||||
|   void defer(void *source, send_message_t *send_message); | ||||
| }; | ||||
|  | ||||
| class APIConnection : public APIServerConnection { | ||||
|  public: | ||||
|   APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); | ||||
| @@ -28,96 +68,140 @@ class APIConnection : public APIServerConnection { | ||||
|   } | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state); | ||||
|   bool send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); | ||||
|   void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); | ||||
|   static bool try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor); | ||||
|   static bool try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, bool state); | ||||
|   static bool try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor); | ||||
| #endif | ||||
| #ifdef USE_COVER | ||||
|   bool send_cover_state(cover::Cover *cover); | ||||
|   bool send_cover_info(cover::Cover *cover); | ||||
|   void send_cover_info(cover::Cover *cover); | ||||
|   static bool try_send_cover_state(APIConnection *api, void *v_cover); | ||||
|   static bool try_send_cover_info(APIConnection *api, void *v_cover); | ||||
|   void cover_command(const CoverCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
|   bool send_fan_state(fan::Fan *fan); | ||||
|   bool send_fan_info(fan::Fan *fan); | ||||
|   void send_fan_info(fan::Fan *fan); | ||||
|   static bool try_send_fan_state(APIConnection *api, void *v_fan); | ||||
|   static bool try_send_fan_info(APIConnection *api, void *v_fan); | ||||
|   void fan_command(const FanCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
|   bool send_light_state(light::LightState *light); | ||||
|   bool send_light_info(light::LightState *light); | ||||
|   void send_light_info(light::LightState *light); | ||||
|   static bool try_send_light_state(APIConnection *api, void *v_light); | ||||
|   static bool try_send_light_info(APIConnection *api, void *v_light); | ||||
|   void light_command(const LightCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
|   bool send_sensor_state(sensor::Sensor *sensor, float state); | ||||
|   bool send_sensor_info(sensor::Sensor *sensor); | ||||
|   void send_sensor_info(sensor::Sensor *sensor); | ||||
|   static bool try_send_sensor_state(APIConnection *api, void *v_sensor); | ||||
|   static bool try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state); | ||||
|   static bool try_send_sensor_info(APIConnection *api, void *v_sensor); | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
|   bool send_switch_state(switch_::Switch *a_switch, bool state); | ||||
|   bool send_switch_info(switch_::Switch *a_switch); | ||||
|   void send_switch_info(switch_::Switch *a_switch); | ||||
|   static bool try_send_switch_state(APIConnection *api, void *v_a_switch); | ||||
|   static bool try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state); | ||||
|   static bool try_send_switch_info(APIConnection *api, void *v_a_switch); | ||||
|   void switch_command(const SwitchCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state); | ||||
|   bool send_text_sensor_info(text_sensor::TextSensor *text_sensor); | ||||
|   void send_text_sensor_info(text_sensor::TextSensor *text_sensor); | ||||
|   static bool try_send_text_sensor_state(APIConnection *api, void *v_text_sensor); | ||||
|   static bool try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, std::string state); | ||||
|   static bool try_send_text_sensor_info(APIConnection *api, void *v_text_sensor); | ||||
| #endif | ||||
| #ifdef USE_ESP32_CAMERA | ||||
|   void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image); | ||||
|   bool send_camera_info(esp32_camera::ESP32Camera *camera); | ||||
|   void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image); | ||||
|   void send_camera_info(esp32_camera::ESP32Camera *camera); | ||||
|   static bool try_send_camera_info(APIConnection *api, void *v_camera); | ||||
|   void camera_image(const CameraImageRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_CLIMATE | ||||
|   bool send_climate_state(climate::Climate *climate); | ||||
|   bool send_climate_info(climate::Climate *climate); | ||||
|   void send_climate_info(climate::Climate *climate); | ||||
|   static bool try_send_climate_state(APIConnection *api, void *v_climate); | ||||
|   static bool try_send_climate_info(APIConnection *api, void *v_climate); | ||||
|   void climate_command(const ClimateCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
|   bool send_number_state(number::Number *number, float state); | ||||
|   bool send_number_info(number::Number *number); | ||||
|   void send_number_info(number::Number *number); | ||||
|   static bool try_send_number_state(APIConnection *api, void *v_number); | ||||
|   static bool try_send_number_state(APIConnection *api, number::Number *number, float state); | ||||
|   static bool try_send_number_info(APIConnection *api, void *v_number); | ||||
|   void number_command(const NumberCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATE | ||||
|   bool send_date_state(datetime::DateEntity *date); | ||||
|   bool send_date_info(datetime::DateEntity *date); | ||||
|   void send_date_info(datetime::DateEntity *date); | ||||
|   static bool try_send_date_state(APIConnection *api, void *v_date); | ||||
|   static bool try_send_date_info(APIConnection *api, void *v_date); | ||||
|   void date_command(const DateCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_DATETIME_TIME | ||||
|   bool send_time_state(datetime::TimeEntity *time); | ||||
|   bool send_time_info(datetime::TimeEntity *time); | ||||
|   void send_time_info(datetime::TimeEntity *time); | ||||
|   static bool try_send_time_state(APIConnection *api, void *v_time); | ||||
|   static bool try_send_time_info(APIConnection *api, void *v_time); | ||||
|   void time_command(const TimeCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
|   bool send_datetime_state(datetime::DateTimeEntity *datetime); | ||||
|   bool send_datetime_info(datetime::DateTimeEntity *datetime); | ||||
|   void send_datetime_info(datetime::DateTimeEntity *datetime); | ||||
|   static bool try_send_datetime_state(APIConnection *api, void *v_datetime); | ||||
|   static bool try_send_datetime_info(APIConnection *api, void *v_datetime); | ||||
|   void datetime_command(const DateTimeCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_TEXT | ||||
|   bool send_text_state(text::Text *text, std::string state); | ||||
|   bool send_text_info(text::Text *text); | ||||
|   void send_text_info(text::Text *text); | ||||
|   static bool try_send_text_state(APIConnection *api, void *v_text); | ||||
|   static bool try_send_text_state(APIConnection *api, text::Text *text, std::string state); | ||||
|   static bool try_send_text_info(APIConnection *api, void *v_text); | ||||
|   void text_command(const TextCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   bool send_select_state(select::Select *select, std::string state); | ||||
|   bool send_select_info(select::Select *select); | ||||
|   void send_select_info(select::Select *select); | ||||
|   static bool try_send_select_state(APIConnection *api, void *v_select); | ||||
|   static bool try_send_select_state(APIConnection *api, select::Select *select, std::string state); | ||||
|   static bool try_send_select_info(APIConnection *api, void *v_select); | ||||
|   void select_command(const SelectCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|   bool send_button_info(button::Button *button); | ||||
|   void send_button_info(button::Button *button); | ||||
|   static bool try_send_button_info(APIConnection *api, void *v_button); | ||||
|   void button_command(const ButtonCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   bool send_lock_state(lock::Lock *a_lock, lock::LockState state); | ||||
|   bool send_lock_info(lock::Lock *a_lock); | ||||
|   void send_lock_info(lock::Lock *a_lock); | ||||
|   static bool try_send_lock_state(APIConnection *api, void *v_a_lock); | ||||
|   static bool try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state); | ||||
|   static bool try_send_lock_info(APIConnection *api, void *v_a_lock); | ||||
|   void lock_command(const LockCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_VALVE | ||||
|   bool send_valve_state(valve::Valve *valve); | ||||
|   bool send_valve_info(valve::Valve *valve); | ||||
|   void send_valve_info(valve::Valve *valve); | ||||
|   static bool try_send_valve_state(APIConnection *api, void *v_valve); | ||||
|   static bool try_send_valve_info(APIConnection *api, void *v_valve); | ||||
|   void valve_command(const ValveCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool send_media_player_state(media_player::MediaPlayer *media_player); | ||||
|   bool send_media_player_info(media_player::MediaPlayer *media_player); | ||||
|   void send_media_player_info(media_player::MediaPlayer *media_player); | ||||
|   static bool try_send_media_player_state(APIConnection *api, void *v_media_player); | ||||
|   static bool try_send_media_player_info(APIConnection *api, void *v_media_player); | ||||
|   void media_player_command(const MediaPlayerCommandRequest &msg) override; | ||||
| #endif | ||||
|   bool send_log_message(int level, const char *tag, const char *line); | ||||
|   bool try_send_log_message(int level, const char *tag, const char *line); | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { | ||||
|     if (!this->service_call_subscription_) | ||||
|       return; | ||||
| @@ -160,18 +244,25 @@ class APIConnection : public APIServerConnection { | ||||
|  | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|   bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); | ||||
|   bool send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); | ||||
|   void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); | ||||
|   static bool try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel); | ||||
|   static bool try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel); | ||||
|   void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_EVENT | ||||
|   bool send_event(event::Event *event, std::string event_type); | ||||
|   bool send_event_info(event::Event *event); | ||||
|   void send_event(event::Event *event, std::string event_type); | ||||
|   void send_event_info(event::Event *event); | ||||
|   static bool try_send_event(APIConnection *api, void *v_event); | ||||
|   static bool try_send_event(APIConnection *api, event::Event *event, std::string event_type); | ||||
|   static bool try_send_event_info(APIConnection *api, void *v_event); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_UPDATE | ||||
|   bool send_update_state(update::UpdateEntity *update); | ||||
|   bool send_update_info(update::UpdateEntity *update); | ||||
|   void send_update_info(update::UpdateEntity *update); | ||||
|   static bool try_send_update_state(APIConnection *api, void *v_update); | ||||
|   static bool try_send_update_info(APIConnection *api, void *v_update); | ||||
|   void update_command(const UpdateCommandRequest &msg) override; | ||||
| #endif | ||||
|  | ||||
| @@ -262,6 +353,7 @@ class APIConnection : public APIServerConnection { | ||||
|   bool service_call_subscription_{false}; | ||||
|   bool next_close_ = false; | ||||
|   APIServer *parent_; | ||||
|   DeferredMessageQueue deferred_message_queue_; | ||||
|   InitialStateIterator initial_state_iterator_; | ||||
|   ListEntitiesIterator list_entities_iterator_; | ||||
|   int state_subs_at_ = -1; | ||||
|   | ||||
| @@ -838,6 +838,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v | ||||
|       this->suggested_area = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 18: { | ||||
|       this->bluetooth_mac_address = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -860,6 +864,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_uint32(14, this->legacy_voice_assistant_version); | ||||
|   buffer.encode_uint32(17, this->voice_assistant_feature_flags); | ||||
|   buffer.encode_string(16, this->suggested_area); | ||||
|   buffer.encode_string(18, this->bluetooth_mac_address); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
| @@ -937,6 +942,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
|   out.append("  suggested_area: "); | ||||
|   out.append("'").append(this->suggested_area).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  bluetooth_mac_address: "); | ||||
|   out.append("'").append(this->bluetooth_mac_address).append("'"); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
|   | ||||
| @@ -354,6 +354,7 @@ class DeviceInfoResponse : public ProtoMessage { | ||||
|   uint32_t legacy_voice_assistant_version{0}; | ||||
|   uint32_t voice_assistant_feature_flags{0}; | ||||
|   std::string suggested_area{}; | ||||
|   std::string bluetooth_mac_address{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
|   | ||||
| @@ -72,7 +72,7 @@ void APIServer::setup() { | ||||
|     logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { | ||||
|       for (auto &c : this->clients_) { | ||||
|         if (!c->remove_) | ||||
|           c->send_log_message(level, tag, message); | ||||
|           c->try_send_log_message(level, tag, message); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| @@ -86,7 +86,7 @@ void APIServer::setup() { | ||||
|         [this](const std::shared_ptr<esp32_camera::CameraImage> &image) { | ||||
|           for (auto &c : this->clients_) { | ||||
|             if (!c->remove_) | ||||
|               c->send_camera_state(image); | ||||
|               c->set_camera_state(image); | ||||
|           } | ||||
|         }); | ||||
|   } | ||||
|   | ||||
| @@ -10,37 +10,63 @@ namespace api { | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { | ||||
|   return this->client_->send_binary_sensor_info(binary_sensor); | ||||
|   this->client_->send_binary_sensor_info(binary_sensor); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_COVER | ||||
| bool ListEntitiesIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_info(cover); } | ||||
| bool ListEntitiesIterator::on_cover(cover::Cover *cover) { | ||||
|   this->client_->send_cover_info(cover); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
| bool ListEntitiesIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_info(fan); } | ||||
| bool ListEntitiesIterator::on_fan(fan::Fan *fan) { | ||||
|   this->client_->send_fan_info(fan); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
| bool ListEntitiesIterator::on_light(light::LightState *light) { return this->client_->send_light_info(light); } | ||||
| bool ListEntitiesIterator::on_light(light::LightState *light) { | ||||
|   this->client_->send_light_info(light); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
| bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_info(sensor); } | ||||
| bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { | ||||
|   this->client_->send_sensor_info(sensor); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
| bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_info(a_switch); } | ||||
| bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { | ||||
|   this->client_->send_switch_info(a_switch); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
| bool ListEntitiesIterator::on_button(button::Button *button) { return this->client_->send_button_info(button); } | ||||
| bool ListEntitiesIterator::on_button(button::Button *button) { | ||||
|   this->client_->send_button_info(button); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
| bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { | ||||
|   return this->client_->send_text_sensor_info(text_sensor); | ||||
|   this->client_->send_text_sensor_info(text_sensor); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
| bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_info(a_lock); } | ||||
| bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { | ||||
|   this->client_->send_lock_info(a_lock); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VALVE | ||||
| bool ListEntitiesIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_info(valve); } | ||||
| bool ListEntitiesIterator::on_valve(valve::Valve *valve) { | ||||
|   this->client_->send_valve_info(valve); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } | ||||
| @@ -52,55 +78,83 @@ bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) { | ||||
|   return this->client_->send_camera_info(camera); | ||||
|   this->client_->send_camera_info(camera); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CLIMATE | ||||
| bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); } | ||||
| bool ListEntitiesIterator::on_climate(climate::Climate *climate) { | ||||
|   this->client_->send_climate_info(climate); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
| bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); } | ||||
| bool ListEntitiesIterator::on_number(number::Number *number) { | ||||
|   this->client_->send_number_info(number); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATE | ||||
| bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_info(date); } | ||||
| bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { | ||||
|   this->client_->send_date_info(date); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_TIME | ||||
| bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_info(time); } | ||||
| bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { | ||||
|   this->client_->send_time_info(time); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
| bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { | ||||
|   return this->client_->send_datetime_info(datetime); | ||||
|   this->client_->send_datetime_info(datetime); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT | ||||
| bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); } | ||||
| bool ListEntitiesIterator::on_text(text::Text *text) { | ||||
|   this->client_->send_text_info(text); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SELECT | ||||
| bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } | ||||
| bool ListEntitiesIterator::on_select(select::Select *select) { | ||||
|   this->client_->send_select_info(select); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { | ||||
|   return this->client_->send_media_player_info(media_player); | ||||
|   this->client_->send_media_player_info(media_player); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
| bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { | ||||
|   return this->client_->send_alarm_control_panel_info(a_alarm_control_panel); | ||||
|   this->client_->send_alarm_control_panel_info(a_alarm_control_panel); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_EVENT | ||||
| bool ListEntitiesIterator::on_event(event::Event *event) { return this->client_->send_event_info(event); } | ||||
| bool ListEntitiesIterator::on_event(event::Event *event) { | ||||
|   this->client_->send_event_info(event); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_UPDATE | ||||
| bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_info(update); } | ||||
| bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { | ||||
|   this->client_->send_update_info(update); | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
|   | ||||
| @@ -80,6 +80,7 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
|   bool on_update(update::UpdateEntity *update) override; | ||||
| #endif | ||||
|   bool on_end() override; | ||||
|   bool completed() { return this->state_ == IteratorState::NONE; } | ||||
|  | ||||
|  protected: | ||||
|   APIConnection *client_; | ||||
|   | ||||
| @@ -76,6 +76,8 @@ class InitialStateIterator : public ComponentIterator { | ||||
| #ifdef USE_UPDATE | ||||
|   bool on_update(update::UpdateEntity *update) override; | ||||
| #endif | ||||
|   bool completed() { return this->state_ == IteratorState::NONE; } | ||||
|  | ||||
|  protected: | ||||
|   APIConnection *client_; | ||||
| }; | ||||
|   | ||||
| @@ -15,6 +15,9 @@ | ||||
|  | ||||
| #include "bluetooth_connection.h" | ||||
|  | ||||
| #include <esp_bt.h> | ||||
| #include <esp_bt_device.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bluetooth_proxy { | ||||
|  | ||||
| @@ -114,6 +117,11 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | ||||
|     return flags; | ||||
|   } | ||||
|  | ||||
|   std::string get_bluetooth_mac_address_pretty() { | ||||
|     const uint8_t *mac = esp_bt_dev_get_address(); | ||||
|     return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); | ||||
|  | ||||
|   | ||||
| @@ -95,7 +95,7 @@ void BMP085Component::read_pressure_() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   uint32_t value = (uint32_t(buffer[0]) << 16) | (uint32_t(buffer[1]) << 8) | uint32_t(buffer[0]); | ||||
|   uint32_t value = (uint32_t(buffer[0]) << 16) | (uint32_t(buffer[1]) << 8) | uint32_t(buffer[2]); | ||||
|   if ((value >> 5) == 0) { | ||||
|     ESP_LOGW(TAG, "Invalid pressure!"); | ||||
|     this->status_set_warning(); | ||||
|   | ||||
							
								
								
									
										2
									
								
								esphome/components/chsc6x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								esphome/components/chsc6x/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| CODEOWNERS = ["@kkosik20"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
							
								
								
									
										47
									
								
								esphome/components/chsc6x/chsc6x_touchscreen.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/chsc6x/chsc6x_touchscreen.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| #include "chsc6x_touchscreen.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace chsc6x { | ||||
|  | ||||
| void CHSC6XTouchscreen::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up CHSC6X Touchscreen..."); | ||||
|   if (this->interrupt_pin_ != nullptr) { | ||||
|     this->interrupt_pin_->setup(); | ||||
|     this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); | ||||
|   } | ||||
|   if (this->x_raw_max_ == this->x_raw_min_) { | ||||
|     this->x_raw_max_ = this->display_->get_native_width(); | ||||
|   } | ||||
|   if (this->y_raw_max_ == this->y_raw_min_) { | ||||
|     this->y_raw_max_ = this->display_->get_native_height(); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "CHSC6X Touchscreen setup complete"); | ||||
| } | ||||
|  | ||||
| void CHSC6XTouchscreen::update_touches() { | ||||
|   uint8_t data[CHSC6X_REG_STATUS_LEN]; | ||||
|   if (!this->read_bytes(CHSC6X_REG_STATUS, data, sizeof(data))) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   uint8_t num_of_touches = data[CHSC6X_REG_STATUS_TOUCH]; | ||||
|  | ||||
|   if (num_of_touches == 1) { | ||||
|     uint16_t x = data[CHSC6X_REG_STATUS_X_COR]; | ||||
|     uint16_t y = data[CHSC6X_REG_STATUS_Y_COR]; | ||||
|     this->add_raw_touch_position_(0, x, y); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void CHSC6XTouchscreen::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "CHSC6X Touchscreen:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   LOG_PIN("  Interrupt Pin: ", this->interrupt_pin_); | ||||
|   ESP_LOGCONFIG(TAG, "  Touch timeout: %d", this->touch_timeout_); | ||||
|   ESP_LOGCONFIG(TAG, "  x_raw_max_: %d", this->x_raw_max_); | ||||
|   ESP_LOGCONFIG(TAG, "  y_raw_max_: %d", this->y_raw_max_); | ||||
| } | ||||
|  | ||||
| }  // namespace chsc6x | ||||
| }  // namespace esphome | ||||
							
								
								
									
										34
									
								
								esphome/components/chsc6x/chsc6x_touchscreen.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/chsc6x/chsc6x_touchscreen.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/touchscreen/touchscreen.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace chsc6x { | ||||
|  | ||||
| static const char *const TAG = "chsc6x.touchscreen"; | ||||
|  | ||||
| static const uint8_t CHSC6X_REG_STATUS = 0x00; | ||||
| static const uint8_t CHSC6X_REG_STATUS_TOUCH = 0x00; | ||||
| static const uint8_t CHSC6X_REG_STATUS_X_COR = 0x02; | ||||
| static const uint8_t CHSC6X_REG_STATUS_Y_COR = 0x04; | ||||
| static const uint8_t CHSC6X_REG_STATUS_LEN = 0x05; | ||||
| static const uint8_t CHSC6X_CHIP_ID = 0x2e; | ||||
|  | ||||
| class CHSC6XTouchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void update_touches() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } | ||||
|  | ||||
|  protected: | ||||
|   InternalGPIOPin *interrupt_pin_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace chsc6x | ||||
| }  // namespace esphome | ||||
							
								
								
									
										33
									
								
								esphome/components/chsc6x/touchscreen.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/chsc6x/touchscreen.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c, touchscreen | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_INTERRUPT_PIN | ||||
|  | ||||
| chsc6x_ns = cg.esphome_ns.namespace("chsc6x") | ||||
|  | ||||
| CHSC6XTouchscreen = chsc6x_ns.class_( | ||||
|     "CHSC6XTouchscreen", | ||||
|     touchscreen.Touchscreen, | ||||
|     i2c.I2CDevice, | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     touchscreen.touchscreen_schema("100ms") | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(CHSC6XTouchscreen), | ||||
|             cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, | ||||
|         } | ||||
|     ) | ||||
|     .extend(i2c.i2c_device_schema(0x2E)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await touchscreen.register_touchscreen(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if interrupt_pin := config.get(CONF_INTERRUPT_PIN): | ||||
|         cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) | ||||
| @@ -128,7 +128,6 @@ VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema( | ||||
|  | ||||
|  | ||||
| def visual_temperature_step(value): | ||||
|  | ||||
|     # Allow defining target/current temperature steps separately | ||||
|     if isinstance(value, dict): | ||||
|         return VISUAL_TEMPERATURE_STEP_SCHEMA(value) | ||||
|   | ||||
| @@ -1,28 +1,5 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor | ||||
|  | ||||
| from .. import cst816_ns | ||||
| from ..touchscreen import CST816Touchscreen, CST816ButtonListener | ||||
|  | ||||
| CONF_CST816_ID = "cst816_id" | ||||
|  | ||||
| CST816Button = cst816_ns.class_( | ||||
|     "CST816Button", | ||||
|     binary_sensor.BinarySensor, | ||||
|     cg.Component, | ||||
|     CST816ButtonListener, | ||||
|     cg.Parented.template(CST816Touchscreen), | ||||
| CONFIG_SCHEMA = cv.invalid( | ||||
|     "The CST816 binary sensor has been removed. Instead use the touchscreen binary sensor with the 'use_raw' flag set." | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST816Button).extend( | ||||
|     { | ||||
|         cv.GenerateID(CONF_CST816_ID): cv.use_id(CST816Touchscreen), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await binary_sensor.new_binary_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await cg.register_parented(var, config[CONF_CST816_ID]) | ||||
|   | ||||
| @@ -1,27 +0,0 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
| #include "esphome/components/cst816/touchscreen/cst816_touchscreen.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace cst816 { | ||||
|  | ||||
| class CST816Button : public binary_sensor::BinarySensor, | ||||
|                      public Component, | ||||
|                      public CST816ButtonListener, | ||||
|                      public Parented<CST816Touchscreen> { | ||||
|  public: | ||||
|   void setup() override { | ||||
|     this->parent_->register_button_listener(this); | ||||
|     this->publish_initial_state(false); | ||||
|   } | ||||
|  | ||||
|   void dump_config() override { LOG_BINARY_SENSOR("", "CST816 Button", this); } | ||||
|  | ||||
|   void update_button(bool state) override { this->publish_state(state); } | ||||
| }; | ||||
|  | ||||
| }  // namespace cst816 | ||||
| }  // namespace esphome | ||||
| @@ -37,14 +37,6 @@ void CST816Touchscreen::continue_setup_() { | ||||
|   ESP_LOGCONFIG(TAG, "CST816 Touchscreen setup complete"); | ||||
| } | ||||
|  | ||||
| void CST816Touchscreen::update_button_state_(bool state) { | ||||
|   if (this->button_touched_ == state) | ||||
|     return; | ||||
|   this->button_touched_ = state; | ||||
|   for (auto *listener : this->button_listeners_) | ||||
|     listener->update_button(state); | ||||
| } | ||||
|  | ||||
| void CST816Touchscreen::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up CST816 Touchscreen..."); | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
| @@ -68,18 +60,13 @@ void CST816Touchscreen::update_touches() { | ||||
|   } | ||||
|   uint8_t num_of_touches = data[REG_TOUCH_NUM] & 3; | ||||
|   if (num_of_touches == 0) { | ||||
|     this->update_button_state_(false); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   uint16_t x = encode_uint16(data[REG_XPOS_HIGH] & 0xF, data[REG_XPOS_LOW]); | ||||
|   uint16_t y = encode_uint16(data[REG_YPOS_HIGH] & 0xF, data[REG_YPOS_LOW]); | ||||
|   ESP_LOGV(TAG, "Read touch %d/%d", x, y); | ||||
|   if (x >= this->x_raw_max_) { | ||||
|     this->update_button_state_(true); | ||||
|   } else { | ||||
|   this->add_raw_touch_position_(0, x, y); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void CST816Touchscreen::dump_config() { | ||||
| @@ -87,6 +74,8 @@ void CST816Touchscreen::dump_config() { | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   LOG_PIN("  Interrupt Pin: ", this->interrupt_pin_); | ||||
|   LOG_PIN("  Reset Pin: ", this->reset_pin_); | ||||
|   ESP_LOGCONFIG(TAG, "  X Raw Min: %d, X Raw Max: %d", this->x_raw_min_, this->x_raw_max_); | ||||
|   ESP_LOGCONFIG(TAG, "  Y Raw Min: %d, Y Raw Max: %d", this->y_raw_min_, this->y_raw_max_); | ||||
|   const char *name; | ||||
|   switch (this->chip_id_) { | ||||
|     case CST820_CHIP_ID: | ||||
|   | ||||
| @@ -40,7 +40,6 @@ class CST816Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void update_touches() override; | ||||
|   void register_button_listener(CST816ButtonListener *listener) { this->button_listeners_.push_back(listener); } | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } | ||||
| @@ -49,14 +48,11 @@ class CST816Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice | ||||
|  | ||||
|  protected: | ||||
|   void continue_setup_(); | ||||
|   void update_button_state_(bool state); | ||||
|  | ||||
|   InternalGPIOPin *interrupt_pin_{}; | ||||
|   GPIOPin *reset_pin_{}; | ||||
|   uint8_t chip_id_{}; | ||||
|   bool skip_probe_{};  // if set, do not expect to be able to probe the controller on the i2c bus. | ||||
|   std::vector<CST816ButtonListener *> button_listeners_; | ||||
|   bool button_touched_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace cst816 | ||||
|   | ||||
| @@ -66,7 +66,9 @@ FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant | ||||
|  | ||||
| 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)] | ||||
|     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]) | ||||
|   | ||||
| @@ -112,8 +112,7 @@ def validate_supports(value): | ||||
|         ) | ||||
|     if is_pullup and num == 16: | ||||
|         raise cv.Invalid( | ||||
|             "GPIO Pin 16 does not support pullup pin mode. " | ||||
|             "Please choose another pin.", | ||||
|             "GPIO Pin 16 does not support pullup pin mode. Please choose another pin.", | ||||
|             [CONF_MODE, CONF_PULLUP], | ||||
|         ) | ||||
|     if is_pulldown and num != 16: | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| from collections.abc import MutableMapping | ||||
| import functools | ||||
| import hashlib | ||||
| import logging | ||||
| @@ -6,10 +7,10 @@ from pathlib import Path | ||||
| import re | ||||
|  | ||||
| import esphome_glyphsets as glyphsets | ||||
| import freetype | ||||
| from freetype import Face, ft_pixel_mode_grays, ft_pixel_mode_mono | ||||
| import requests | ||||
|  | ||||
| from esphome import core, external_files | ||||
| from esphome import external_files | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -26,7 +27,7 @@ from esphome.const import ( | ||||
|     CONF_WEIGHT, | ||||
| ) | ||||
| from esphome.core import CORE, HexInt | ||||
| from esphome.helpers import copy_file_if_changed, cpp_string_escape | ||||
| from esphome.helpers import cpp_string_escape | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -49,13 +50,42 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs" | ||||
|  | ||||
|  | ||||
| # Cache loaded freetype fonts | ||||
| class FontCache(dict): | ||||
|     def __missing__(self, key): | ||||
|         try: | ||||
|             res = self[key] = freetype.Face(key) | ||||
|             return res | ||||
|         except freetype.FT_Exception as e: | ||||
|             raise cv.Invalid(f"Could not load Font file {key}: {e}") from e | ||||
| class FontCache(MutableMapping): | ||||
|     @staticmethod | ||||
|     def get_name(value): | ||||
|         if CONF_FAMILY in value: | ||||
|             return ( | ||||
|                 f"{value[CONF_FAMILY]}:{int(value[CONF_ITALIC])}:{value[CONF_WEIGHT]}" | ||||
|             ) | ||||
|         if CONF_URL in value: | ||||
|             return value[CONF_URL] | ||||
|         return value[CONF_PATH] | ||||
|  | ||||
|     @staticmethod | ||||
|     def _keytransform(value): | ||||
|         if CONF_FAMILY in value: | ||||
|             return f"gfont:{value[CONF_FAMILY]}:{int(value[CONF_ITALIC])}:{value[CONF_WEIGHT]}" | ||||
|         if CONF_URL in value: | ||||
|             return f"url:{value[CONF_URL]}" | ||||
|         return f"file:{value[CONF_PATH]}" | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.store = {} | ||||
|  | ||||
|     def __delitem__(self, key): | ||||
|         del self.store[self._keytransform(key)] | ||||
|  | ||||
|     def __iter__(self): | ||||
|         return iter(self.store) | ||||
|  | ||||
|     def __len__(self): | ||||
|         return len(self.store) | ||||
|  | ||||
|     def __getitem__(self, item): | ||||
|         return self.store[self._keytransform(item)] | ||||
|  | ||||
|     def __setitem__(self, key, value): | ||||
|         self.store[self._keytransform(key)] = Face(str(value)) | ||||
|  | ||||
|  | ||||
| FONT_CACHE = FontCache() | ||||
| @@ -109,14 +139,14 @@ def check_missing_glyphs(file, codepoints, warning: bool = False): | ||||
|         ) | ||||
|         if count > 10: | ||||
|             missing_str += f"\n    and {count - 10} more." | ||||
|         message = f"Font {Path(file).name} is missing {count} glyph{'s' if count != 1 else ''}:\n    {missing_str}" | ||||
|         message = f"Font {FontCache.get_name(file)} is missing {count} glyph{'s' if count != 1 else ''}:\n    {missing_str}" | ||||
|         if warning: | ||||
|             _LOGGER.warning(message) | ||||
|         else: | ||||
|             raise cv.Invalid(message) | ||||
|  | ||||
|  | ||||
| def validate_glyphs(config): | ||||
| def validate_font_config(config): | ||||
|     """ | ||||
|     Check for duplicate codepoints, then check that all requested codepoints actually | ||||
|     have glyphs defined in the appropriate font file. | ||||
| @@ -143,8 +173,6 @@ def validate_glyphs(config): | ||||
|     # Make setpoints and glyphspoints disjoint | ||||
|     setpoints.difference_update(glyphspoints) | ||||
|     if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP: | ||||
|         # Pillow only allows 256 glyphs per bitmap font. Not sure if that is a Pillow limitation | ||||
|         # or a file format limitation | ||||
|         if any(x >= 256 for x in setpoints.copy().union(glyphspoints)): | ||||
|             raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255") | ||||
|     else: | ||||
| @@ -154,13 +182,14 @@ def validate_glyphs(config): | ||||
|             points = {ord(x) for x in flatten(extra[CONF_GLYPHS])} | ||||
|             glyphspoints.difference_update(points) | ||||
|             setpoints.difference_update(points) | ||||
|             check_missing_glyphs(extra[CONF_FILE][CONF_PATH], points) | ||||
|             check_missing_glyphs(extra[CONF_FILE], points) | ||||
|  | ||||
|         # A named glyph that can't be provided is an error | ||||
|         check_missing_glyphs(fileconf[CONF_PATH], glyphspoints) | ||||
|  | ||||
|         check_missing_glyphs(fileconf, glyphspoints) | ||||
|         # A missing glyph from a set is a warning. | ||||
|         if not config[CONF_IGNORE_MISSING_GLYPHS]: | ||||
|             check_missing_glyphs(fileconf[CONF_PATH], setpoints, warning=True) | ||||
|             check_missing_glyphs(fileconf, setpoints, warning=True) | ||||
|  | ||||
|     # Populate the default after the above checks so that use of the default doesn't trigger errors | ||||
|     if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]: | ||||
| @@ -168,17 +197,32 @@ def validate_glyphs(config): | ||||
|             config[CONF_GLYPHS] = [DEFAULT_GLYPHS] | ||||
|         else: | ||||
|             # set a default glyphset, intersected with what the font actually offers | ||||
|             font = FONT_CACHE[fileconf[CONF_PATH]] | ||||
|             font = FONT_CACHE[fileconf] | ||||
|             config[CONF_GLYPHS] = [ | ||||
|                 chr(x) | ||||
|                 for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET) | ||||
|                 if font.get_char_index(x) != 0 | ||||
|             ] | ||||
|  | ||||
|     if config[CONF_FILE][CONF_TYPE] == TYPE_LOCAL_BITMAP: | ||||
|         if CONF_SIZE in config: | ||||
|             raise cv.Invalid( | ||||
|                 "Size is not a valid option for bitmap fonts, which are inherently fixed size" | ||||
|             ) | ||||
|     elif CONF_SIZE not in config: | ||||
|         config[CONF_SIZE] = 20 | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FONT_EXTENSIONS = (".ttf", ".woff", ".otf") | ||||
| BITMAP_EXTENSIONS = (".bdf", ".pcf") | ||||
|  | ||||
|  | ||||
| def validate_bitmap_file(value): | ||||
|     if not any(map(value.lower().endswith, BITMAP_EXTENSIONS)): | ||||
|         raise cv.Invalid(f"Only {', '.join(BITMAP_EXTENSIONS)} files are supported.") | ||||
|     return CORE.relative_config_path(cv.file_(value)) | ||||
|  | ||||
|  | ||||
| def validate_truetype_file(value): | ||||
| @@ -187,24 +231,40 @@ def validate_truetype_file(value): | ||||
|             f"Please unzip the font archive '{value}' first and then use the .ttf files inside." | ||||
|         ) | ||||
|     if not any(map(value.lower().endswith, FONT_EXTENSIONS)): | ||||
|         raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.") | ||||
|         raise cv.Invalid(f"Only {', '.join(FONT_EXTENSIONS)} files are supported.") | ||||
|     return CORE.relative_config_path(cv.file_(value)) | ||||
|  | ||||
|  | ||||
| def add_local_file(value): | ||||
|     if value in FONT_CACHE: | ||||
|         return value | ||||
|     path = value[CONF_PATH] | ||||
|     if not os.path.isfile(path): | ||||
|         raise cv.Invalid(f"File '{path}' not found.") | ||||
|     FONT_CACHE[value] = path | ||||
|     return value | ||||
|  | ||||
|  | ||||
| TYPE_LOCAL = "local" | ||||
| TYPE_LOCAL_BITMAP = "local_bitmap" | ||||
| TYPE_GFONTS = "gfonts" | ||||
| TYPE_WEB = "web" | ||||
| LOCAL_SCHEMA = cv.Schema( | ||||
| LOCAL_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_PATH): validate_truetype_file, | ||||
|         } | ||||
|     ), | ||||
|     add_local_file, | ||||
| ) | ||||
|  | ||||
| LOCAL_BITMAP_SCHEMA = cv.Schema( | ||||
| LOCAL_BITMAP_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|         cv.Required(CONF_PATH): cv.file_, | ||||
|             cv.Required(CONF_PATH): validate_bitmap_file, | ||||
|         } | ||||
|     ), | ||||
|     add_local_file, | ||||
| ) | ||||
|  | ||||
| FULLPATH_SCHEMA = cv.maybe_simple_value( | ||||
| @@ -235,27 +295,23 @@ def _compute_local_font_path(value: dict) -> Path: | ||||
|     h.update(url.encode()) | ||||
|     key = h.hexdigest()[:8] | ||||
|     base_dir = external_files.compute_local_file_dir(DOMAIN) | ||||
|     _LOGGER.debug("_compute_local_font_path: base_dir=%s", base_dir / key) | ||||
|     _LOGGER.debug("_compute_local_font_path: %s", base_dir / key) | ||||
|     return base_dir / key | ||||
|  | ||||
|  | ||||
| def get_font_path(value, font_type) -> Path: | ||||
|     if font_type == TYPE_GFONTS: | ||||
|         name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" | ||||
|         return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf" | ||||
|     if font_type == TYPE_WEB: | ||||
|         return _compute_local_font_path(value) / "font.ttf" | ||||
|     assert False | ||||
|  | ||||
|  | ||||
| def download_gfont(value): | ||||
|     if value in FONT_CACHE: | ||||
|         return value | ||||
|     name = ( | ||||
|         f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}" | ||||
|     ) | ||||
|     url = f"https://fonts.googleapis.com/css2?family={name}" | ||||
|     path = get_font_path(value, TYPE_GFONTS) | ||||
|     path = ( | ||||
|         external_files.compute_local_file_dir(DOMAIN) | ||||
|         / f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1.ttf" | ||||
|     ) | ||||
|     if not external_files.is_file_recent(str(path), value[CONF_REFRESH]): | ||||
|         _LOGGER.debug("download_gfont: path=%s", path) | ||||
|  | ||||
|         try: | ||||
|             req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT) | ||||
|             req.raise_for_status() | ||||
| @@ -275,16 +331,23 @@ def download_gfont(value): | ||||
|         _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url) | ||||
|  | ||||
|         external_files.download_content(ttf_url, path) | ||||
|     return FULLPATH_SCHEMA(path) | ||||
|         # In case the remote file is not modified, the download_content function will return the existing file, | ||||
|         # so update the modification time to now. | ||||
|         path.touch() | ||||
|     FONT_CACHE[value] = path | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def download_web_font(value): | ||||
|     if value in FONT_CACHE: | ||||
|         return value | ||||
|     url = value[CONF_URL] | ||||
|     path = get_font_path(value, TYPE_WEB) | ||||
|     path = _compute_local_font_path(value) / "font.ttf" | ||||
|  | ||||
|     external_files.download_content(url, path) | ||||
|     _LOGGER.debug("download_web_font: path=%s", path) | ||||
|     return FULLPATH_SCHEMA(path) | ||||
|     FONT_CACHE[value] = path | ||||
|     return value | ||||
|  | ||||
|  | ||||
| EXTERNAL_FONT_SCHEMA = cv.Schema( | ||||
| @@ -340,14 +403,14 @@ def validate_file_shorthand(value): | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     if value.endswith(".pcf") or value.endswith(".bdf"): | ||||
|         value = convert_bitmap_to_pillow_font( | ||||
|             CORE.relative_config_path(cv.file_(value)) | ||||
|         ) | ||||
|         return { | ||||
|     extension = Path(value).suffix | ||||
|     if extension in BITMAP_EXTENSIONS: | ||||
|         return font_file_schema( | ||||
|             { | ||||
|                 CONF_TYPE: TYPE_LOCAL_BITMAP, | ||||
|                 CONF_PATH: value, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     return font_file_schema( | ||||
|         { | ||||
| @@ -391,7 +454,7 @@ FONT_SCHEMA = cv.Schema( | ||||
|             cv.one_of(*glyphsets.defined_glyphsets()) | ||||
|         ), | ||||
|         cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean, | ||||
|         cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), | ||||
|         cv.Optional(CONF_SIZE): cv.int_range(min=1), | ||||
|         cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8), | ||||
|         cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list( | ||||
|             cv.Schema( | ||||
| @@ -406,114 +469,19 @@ FONT_SCHEMA = cv.Schema( | ||||
|     }, | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_glyphs) | ||||
|  | ||||
|  | ||||
| # PIL doesn't provide a consistent interface for both TrueType and bitmap | ||||
| # fonts. So, we use our own wrappers to give us the consistency that we need. | ||||
|  | ||||
|  | ||||
| class TrueTypeFontWrapper: | ||||
|     def __init__(self, font): | ||||
|         self.font = font | ||||
|  | ||||
|     def getoffset(self, glyph): | ||||
|         _, (offset_x, offset_y) = self.font.font.getsize(glyph) | ||||
|         return offset_x, offset_y | ||||
|  | ||||
|     def getmask(self, glyph, **kwargs): | ||||
|         return self.font.getmask(str(glyph), **kwargs) | ||||
|  | ||||
|     def getmetrics(self, glyphs): | ||||
|         return self.font.getmetrics() | ||||
|  | ||||
|  | ||||
| class BitmapFontWrapper: | ||||
|     def __init__(self, font): | ||||
|         self.font = font | ||||
|         self.max_height = 0 | ||||
|  | ||||
|     def getoffset(self, glyph): | ||||
|         return 0, 0 | ||||
|  | ||||
|     def getmask(self, glyph, **kwargs): | ||||
|         return self.font.getmask(str(glyph), **kwargs) | ||||
|  | ||||
|     def getmetrics(self, glyphs): | ||||
|         max_height = 0 | ||||
|         for glyph in glyphs: | ||||
|             mask = self.getmask(glyph, mode="1") | ||||
|             _, height = mask.size | ||||
|             max_height = max(max_height, height) | ||||
|         return max_height, 0 | ||||
| CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_font_config) | ||||
|  | ||||
|  | ||||
| class EFont: | ||||
|     def __init__(self, file, size, codepoints): | ||||
|     def __init__(self, file, codepoints): | ||||
|         self.codepoints = codepoints | ||||
|         path = file[CONF_PATH] | ||||
|         self.name = Path(path).name | ||||
|         ftype = file[CONF_TYPE] | ||||
|         if ftype == TYPE_LOCAL_BITMAP: | ||||
|             self.font = load_bitmap_font(path) | ||||
|         else: | ||||
|             self.font = load_ttf_font(path, size) | ||||
|         self.ascent, self.descent = self.font.getmetrics(codepoints) | ||||
|  | ||||
|  | ||||
| def convert_bitmap_to_pillow_font(filepath): | ||||
|     from PIL import BdfFontFile, PcfFontFile | ||||
|  | ||||
|     local_bitmap_font_file = external_files.compute_local_file_dir( | ||||
|         DOMAIN, | ||||
|     ) / os.path.basename(filepath) | ||||
|  | ||||
|     copy_file_if_changed(filepath, local_bitmap_font_file) | ||||
|  | ||||
|     local_pil_font_file = local_bitmap_font_file.with_suffix(".pil") | ||||
|     with open(local_bitmap_font_file, "rb") as fp: | ||||
|         try: | ||||
|             try: | ||||
|                 p = PcfFontFile.PcfFontFile(fp) | ||||
|             except SyntaxError: | ||||
|                 fp.seek(0) | ||||
|                 p = BdfFontFile.BdfFontFile(fp) | ||||
|  | ||||
|             # Convert to pillow-formatted fonts, which have a .pil and .pbm extension. | ||||
|             p.save(local_pil_font_file) | ||||
|         except (SyntaxError, OSError) as err: | ||||
|             raise core.EsphomeError( | ||||
|                 f"Failed to parse as bitmap font: '{filepath}': {err}" | ||||
|             ) | ||||
|  | ||||
|     return str(local_pil_font_file) | ||||
|  | ||||
|  | ||||
| def load_bitmap_font(filepath): | ||||
|     from PIL import ImageFont | ||||
|  | ||||
|     try: | ||||
|         font = ImageFont.load(str(filepath)) | ||||
|     except Exception as e: | ||||
|         raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}") | ||||
|  | ||||
|     return BitmapFontWrapper(font) | ||||
|  | ||||
|  | ||||
| def load_ttf_font(path, size): | ||||
|     from PIL import ImageFont | ||||
|  | ||||
|     try: | ||||
|         font = ImageFont.truetype(str(path), size) | ||||
|     except Exception as e: | ||||
|         raise core.EsphomeError(f"Could not load TrueType file {path}: {e}") | ||||
|  | ||||
|     return TrueTypeFontWrapper(font) | ||||
|         self.font: Face = FONT_CACHE[file] | ||||
|  | ||||
|  | ||||
| class GlyphInfo: | ||||
|     def __init__(self, data_len, offset_x, offset_y, width, height): | ||||
|     def __init__(self, data_len, advance, offset_x, offset_y, width, height): | ||||
|         self.data_len = data_len | ||||
|         self.advance = advance | ||||
|         self.offset_x = offset_x | ||||
|         self.offset_y = offset_y | ||||
|         self.width = width | ||||
| @@ -537,15 +505,14 @@ async def to_code(config): | ||||
|     } | ||||
|     # get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets | ||||
|     point_set.update(flatten(config[CONF_GLYPHS])) | ||||
|     size = config[CONF_SIZE] | ||||
|     # Create the codepoint to font file map | ||||
|     base_font = EFont(config[CONF_FILE], size, point_set) | ||||
|     point_font_map: dict[str, EFont] = {c: base_font for c in point_set} | ||||
|     base_font = FONT_CACHE[config[CONF_FILE]] | ||||
|     point_font_map: dict[str, Face] = {c: base_font for c in point_set} | ||||
|     # process extras, updating the map and extending the codepoint list | ||||
|     for extra in config[CONF_EXTRAS]: | ||||
|         extra_points = flatten(extra[CONF_GLYPHS]) | ||||
|         point_set.update(extra_points) | ||||
|         extra_font = EFont(extra[CONF_FILE], size, extra_points) | ||||
|         extra_font = FONT_CACHE[extra[CONF_FILE]] | ||||
|         point_font_map.update({c: extra_font for c in extra_points}) | ||||
|  | ||||
|     codepoints = list(point_set) | ||||
| @@ -553,28 +520,52 @@ async def to_code(config): | ||||
|     glyph_args = {} | ||||
|     data = [] | ||||
|     bpp = config[CONF_BPP] | ||||
|     if bpp == 1: | ||||
|         mode = "1" | ||||
|         scale = 1 | ||||
|     else: | ||||
|         mode = "L" | ||||
|     mode = ft_pixel_mode_grays | ||||
|     scale = 256 // (1 << bpp) | ||||
|     # create the data array for all glyphs | ||||
|     for codepoint in codepoints: | ||||
|         font = point_font_map[codepoint] | ||||
|         mask = font.font.getmask(codepoint, mode=mode) | ||||
|         offset_x, offset_y = font.font.getoffset(codepoint) | ||||
|         width, height = mask.size | ||||
|         if not font.has_fixed_sizes: | ||||
|             font.set_pixel_sizes(config[CONF_SIZE], 0) | ||||
|         font.load_char(codepoint) | ||||
|         font.glyph.render(mode) | ||||
|         width = font.glyph.bitmap.width | ||||
|         height = font.glyph.bitmap.rows | ||||
|         buffer = font.glyph.bitmap.buffer | ||||
|         pitch = font.glyph.bitmap.pitch | ||||
|         glyph_data = [0] * ((height * width * bpp + 7) // 8) | ||||
|         src_mode = font.glyph.bitmap.pixel_mode | ||||
|         pos = 0 | ||||
|         for y in range(height): | ||||
|             for x in range(width): | ||||
|                 pixel = mask.getpixel((x, y)) // scale | ||||
|                 if src_mode == ft_pixel_mode_mono: | ||||
|                     pixel = ( | ||||
|                         (1 << bpp) - 1 | ||||
|                         if buffer[y * pitch + x // 8] & (1 << (7 - x % 8)) | ||||
|                         else 0 | ||||
|                     ) | ||||
|                 else: | ||||
|                     pixel = buffer[y * pitch + x] // scale | ||||
|                 for bit_num in range(bpp): | ||||
|                     if pixel & (1 << (bpp - bit_num - 1)): | ||||
|                         glyph_data[pos // 8] |= 0x80 >> (pos % 8) | ||||
|                     pos += 1 | ||||
|         glyph_args[codepoint] = GlyphInfo(len(data), offset_x, offset_y, width, height) | ||||
|         ascender = font.size.ascender // 64 | ||||
|         if ascender == 0: | ||||
|             if font.has_fixed_sizes: | ||||
|                 ascender = font.available_sizes[0].height | ||||
|             else: | ||||
|                 _LOGGER.error( | ||||
|                     "Unable to determine ascender of font %s", config[CONF_FILE] | ||||
|                 ) | ||||
|         glyph_args[codepoint] = GlyphInfo( | ||||
|             len(data), | ||||
|             font.glyph.metrics.horiAdvance // 64, | ||||
|             font.glyph.bitmap_left, | ||||
|             ascender - font.glyph.bitmap_top, | ||||
|             width, | ||||
|             height, | ||||
|         ) | ||||
|         data += glyph_data | ||||
|  | ||||
|     rhs = [HexInt(x) for x in data] | ||||
| @@ -598,6 +589,7 @@ async def to_code(config): | ||||
|                         f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}" | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("advance", glyph_args[codepoint].advance), | ||||
|                 ("offset_x", glyph_args[codepoint].offset_x), | ||||
|                 ("offset_y", glyph_args[codepoint].offset_y), | ||||
|                 ("width", glyph_args[codepoint].width), | ||||
| @@ -607,11 +599,19 @@ async def to_code(config): | ||||
|  | ||||
|     glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) | ||||
|  | ||||
|     font_height = base_font.size.height // 64 | ||||
|     ascender = base_font.size.ascender // 64 | ||||
|     if font_height == 0: | ||||
|         if base_font.has_fixed_sizes: | ||||
|             font_height = base_font.available_sizes[0].height | ||||
|             ascender = font_height | ||||
|         else: | ||||
|             _LOGGER.error("Unable to determine height of font %s", config[CONF_FILE]) | ||||
|     cg.new_Pvariable( | ||||
|         config[CONF_ID], | ||||
|         glyphs, | ||||
|         len(glyph_initializer), | ||||
|         base_font.ascent, | ||||
|         base_font.ascent + base_font.descent, | ||||
|         ascender, | ||||
|         font_height, | ||||
|         bpp, | ||||
|     ) | ||||
|   | ||||
| @@ -81,7 +81,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in | ||||
|     if (glyph_n < 0) { | ||||
|       // Unknown char, skip | ||||
|       if (!this->get_glyphs().empty()) | ||||
|         x += this->get_glyphs()[0].glyph_data_->width; | ||||
|         x += this->get_glyphs()[0].glyph_data_->advance; | ||||
|       i++; | ||||
|       continue; | ||||
|     } | ||||
| @@ -92,7 +92,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in | ||||
|     } else { | ||||
|       min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); | ||||
|     } | ||||
|     x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; | ||||
|     x += glyph.glyph_data_->advance; | ||||
|  | ||||
|     i += match_length; | ||||
|     has_char = true; | ||||
| @@ -111,7 +111,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo | ||||
|       // Unknown char, skip | ||||
|       ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); | ||||
|       if (!this->get_glyphs().empty()) { | ||||
|         uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width; | ||||
|         uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance; | ||||
|         display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); | ||||
|         x_at += glyph_width; | ||||
|       } | ||||
| @@ -161,7 +161,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; | ||||
|     x_at += glyph.glyph_data_->advance; | ||||
|  | ||||
|     i += match_length; | ||||
|   } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ class Font; | ||||
| struct GlyphData { | ||||
|   const uint8_t *a_char; | ||||
|   const uint8_t *data; | ||||
|   int advance; | ||||
|   int offset_x; | ||||
|   int offset_y; | ||||
|   int width; | ||||
|   | ||||
| @@ -1,9 +1,15 @@ | ||||
| import logging | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| import esphome.final_validate as fv | ||||
| from esphome.components import uart, climate, logger | ||||
| import logging | ||||
|  | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import climate, logger, uart | ||||
| from esphome.components.climate import ( | ||||
|     CONF_CURRENT_TEMPERATURE, | ||||
|     ClimateMode, | ||||
|     ClimatePreset, | ||||
|     ClimateSwingMode, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BEEPER, | ||||
|     CONF_DISPLAY, | ||||
| @@ -24,12 +30,7 @@ from esphome.const import ( | ||||
|     CONF_VISUAL, | ||||
|     CONF_WIFI, | ||||
| ) | ||||
| from esphome.components.climate import ( | ||||
|     ClimateMode, | ||||
|     ClimatePreset, | ||||
|     ClimateSwingMode, | ||||
|     CONF_CURRENT_TEMPERATURE, | ||||
| ) | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,7 @@ float HBridgeSwitch::get_setup_priority() const { return setup_priority::HARDWAR | ||||
| void HBridgeSwitch::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up H-Bridge Switch '%s'...", this->name_.c_str()); | ||||
|  | ||||
|   optional<bool> initial_state = this->get_initial_state_with_restore_mode().value_or(false); | ||||
|   optional<bool> initial_state = this->get_initial_state_with_restore_mode(); | ||||
|  | ||||
|   // Like GPIOSwitch does, set the pin state both before and after pin setup() | ||||
|   this->on_pin_->digital_write(false); | ||||
| @@ -24,7 +24,7 @@ void HBridgeSwitch::setup() { | ||||
|   this->off_pin_->digital_write(false); | ||||
|  | ||||
|   if (initial_state.has_value()) | ||||
|     this->write_state(initial_state); | ||||
|     this->write_state(initial_state.value()); | ||||
| } | ||||
|  | ||||
| void HBridgeSwitch::dump_config() { | ||||
|   | ||||
| @@ -53,6 +53,7 @@ PROTOCOLS = { | ||||
|     "mitsubishi_sez": Protocol.PROTOCOL_MITSUBISHI_SEZ, | ||||
|     "panasonic_ckp": Protocol.PROTOCOL_PANASONIC_CKP, | ||||
|     "panasonic_dke": Protocol.PROTOCOL_PANASONIC_DKE, | ||||
|     "panasonic_eke": Protocol.PROTOCOL_PANASONIC_EKE, | ||||
|     "panasonic_jke": Protocol.PROTOCOL_PANASONIC_JKE, | ||||
|     "panasonic_lke": Protocol.PROTOCOL_PANASONIC_LKE, | ||||
|     "panasonic_nke": Protocol.PROTOCOL_PANASONIC_NKE, | ||||
| @@ -127,6 +128,6 @@ def to_code(config): | ||||
|     cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) | ||||
|     cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) | ||||
|  | ||||
|     cg.add_library("tonia/HeatpumpIR", "1.0.27") | ||||
|     cg.add_library("tonia/HeatpumpIR", "1.0.32") | ||||
|     if CORE.is_libretiny: | ||||
|         CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") | ||||
|   | ||||
| @@ -47,6 +47,7 @@ const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP | ||||
|     {PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }},           // NOLINT | ||||
|     {PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }},                 // NOLINT | ||||
|     {PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }},                 // NOLINT | ||||
|     {PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }},                 // NOLINT | ||||
|     {PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }},                 // NOLINT | ||||
|     {PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }},                 // NOLINT | ||||
|     {PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }},                 // NOLINT | ||||
|   | ||||
| @@ -47,6 +47,7 @@ enum Protocol { | ||||
|   PROTOCOL_MITSUBISHI_SEZ, | ||||
|   PROTOCOL_PANASONIC_CKP, | ||||
|   PROTOCOL_PANASONIC_DKE, | ||||
|   PROTOCOL_PANASONIC_EKE, | ||||
|   PROTOCOL_PANASONIC_JKE, | ||||
|   PROTOCOL_PANASONIC_LKE, | ||||
|   PROTOCOL_PANASONIC_NKE, | ||||
|   | ||||
| @@ -1,23 +1,23 @@ | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| import esphome.final_validate as fv | ||||
| from esphome import pins | ||||
| from esphome.const import ( | ||||
|     CONF_ADDRESS, | ||||
|     CONF_FREQUENCY, | ||||
|     CONF_TIMEOUT, | ||||
|     CONF_I2C_ID, | ||||
|     CONF_ID, | ||||
|     CONF_INPUT, | ||||
|     CONF_OUTPUT, | ||||
|     CONF_SCAN, | ||||
|     CONF_SCL, | ||||
|     CONF_SDA, | ||||
|     CONF_ADDRESS, | ||||
|     CONF_I2C_ID, | ||||
|     CONF_TIMEOUT, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_RP2040, | ||||
| ) | ||||
| from esphome.core import coroutine_with_priority, CORE | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| i2c_ns = cg.esphome_ns.namespace("i2c") | ||||
|   | ||||
| @@ -8,6 +8,10 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
| #define SOC_HP_I2C_NUM SOC_I2C_NUM | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace i2c { | ||||
|  | ||||
| @@ -17,14 +21,14 @@ void IDFI2CBus::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up I2C bus..."); | ||||
|   static i2c_port_t next_port = I2C_NUM_0; | ||||
|   port_ = next_port; | ||||
| #if SOC_I2C_NUM > 1 | ||||
| #if SOC_HP_I2C_NUM > 1 | ||||
|   next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; | ||||
| #else | ||||
|   next_port = I2C_NUM_MAX; | ||||
| #endif | ||||
|  | ||||
|   if (port_ == I2C_NUM_MAX) { | ||||
|     ESP_LOGE(TAG, "Too many I2C buses configured. Max %u supported.", SOC_I2C_NUM); | ||||
|     ESP_LOGE(TAG, "Too many I2C buses configured. Max %u supported.", SOC_HP_I2C_NUM); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   | ||||
| @@ -203,7 +203,7 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick | ||||
|     this->start(); | ||||
|   } | ||||
|  | ||||
|   if ((this->state_ != speaker::STATE_RUNNING) || (this->audio_ring_buffer_.use_count() == 1)) { | ||||
|   if ((this->state_ != speaker::STATE_RUNNING) || (this->audio_ring_buffer_.use_count() != 1)) { | ||||
|     // Unable to write data to a running speaker, so delay the max amount of time so it can get ready | ||||
|     vTaskDelay(ticks_to_wait); | ||||
|     ticks_to_wait = 0; | ||||
|   | ||||
| @@ -57,6 +57,7 @@ ColorOrder = display.display_ns.enum("ColorMode") | ||||
|  | ||||
| MODELS = { | ||||
|     "GC9A01A": ili9xxx_ns.class_("ILI9XXXGC9A01A", ILI9XXXDisplay), | ||||
|     "GC9D01N": ili9xxx_ns.class_("ILI9XXXGC9D01N", ILI9XXXDisplay), | ||||
|     "M5STACK": ili9xxx_ns.class_("ILI9XXXM5Stack", ILI9XXXDisplay), | ||||
|     "M5CORE": ili9xxx_ns.class_("ILI9XXXM5CORE", ILI9XXXDisplay), | ||||
|     "TFT_2.4": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay), | ||||
|   | ||||
| @@ -272,6 +272,11 @@ class ILI9XXXGC9A01A : public ILI9XXXDisplay { | ||||
|   ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240) {} | ||||
| }; | ||||
|  | ||||
| class ILI9XXXGC9D01N : public ILI9XXXDisplay { | ||||
|  public: | ||||
|   ILI9XXXGC9D01N() : ILI9XXXDisplay(INITCMD_GC9D01N, 160, 160) {} | ||||
| }; | ||||
|  | ||||
| //-----------   ILI9XXX_24_TFT display -------------- | ||||
| class ILI9XXXST7735 : public ILI9XXXDisplay { | ||||
|  public: | ||||
|   | ||||
| @@ -367,6 +367,65 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = { | ||||
|   0x00                  // End of list | ||||
| }; | ||||
|  | ||||
| static const uint8_t PROGMEM INITCMD_GC9D01N[] = { | ||||
|   // Enable Inter_command | ||||
|   0xFE, 0,          // Inter Register Enable 1 (FEh) | ||||
|   0xEF, 0,          // Inter Register Enable 2 (EFh) | ||||
|   // Inter_command is now enabled | ||||
|   0x80, 1, 0xFF, | ||||
|   0x81, 1, 0xFF, | ||||
|   0x82, 1, 0xFF, | ||||
|   0x83, 1, 0xFF, | ||||
|   0x84, 1, 0xFF, | ||||
|   0x85, 1, 0xFF, | ||||
|   0x86, 1, 0xFF, | ||||
|   0x87, 1, 0xFF, | ||||
|   0x88, 1, 0xFF, | ||||
|   0x89, 1, 0xFF, | ||||
|   0x8A, 1, 0xFF, | ||||
|   0x8B, 1, 0xFF, | ||||
|   0x8C, 1, 0xFF, | ||||
|   0x8D, 1, 0xFF, | ||||
|   0x8E, 1, 0xFF, | ||||
|   0x8F, 1, 0xFF, | ||||
|   0X3A, 1, 0x05,    // COLMOD: Pixel Format Set (3Ah) MCU interface, 16 bits / pixel | ||||
|   0xEC, 1, 0x01,    // Inversion (ECh) DINV=1+2H1V column for Dual Gate (BFh=0) | ||||
|                     // According to datasheet Inversion (ECh) value 0x01 isn't valid, but Lilygo uses it everywhere | ||||
|   0x74, 7, 0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||
|   0x98, 1, 0x3e, | ||||
|   0x99, 1, 0x3e, | ||||
|   0xB5, 2, 0x0D, 0x0D,    // Blanking Porch Control (B5h) VFP=14 VBP=14 HBP=Off | ||||
|   0x60, 4, 0x38, 0x0F, 0x79, 0x67, | ||||
|   0x61, 4, 0x38, 0x11, 0x79, 0x67, | ||||
|   0x64, 6, 0x38, 0x17, 0x71, 0x5F, 0x79, 0x67, | ||||
|   0x65, 6, 0x38, 0x13, 0x71, 0x5B, 0x79, 0x67, | ||||
|   0x6A, 2, 0x00, 0x00, | ||||
|   0x6C, 7, 0x22, 0x02, 0x22, 0x02, 0x22, 0x22, 0x50, | ||||
|   0x6E, 32, 0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0x0F, 0x0F, | ||||
|             0x0D, 0x0D, 0x0B, 0x0B, 0x09, 0x09, 0x00, 0x00, | ||||
|             0x00, 0x00, 0x0A, 0x0A, 0x0C, 0x0C, 0x0E, 0x0E, | ||||
|             0x10, 0x10, 0x00, 0x00, 0x02, 0x02, 0x04, 0x04, | ||||
|   0xBF, 1, 0x01,    // Dual-Single gate select (BFh) 01h = dual gate mode | ||||
|   0xF9, 1, 0x40, | ||||
|   0x9B, 5, 0x3B, 0x93, 0x33, 0x7F, 0x00, | ||||
|   0x7E, 1, 0x30, | ||||
|   0x70, 6, 0x0D, 0x02, 0x08, 0x0D, 0x02, 0x08, | ||||
|   0x71, 3, 0x0D, 0x02, 0x08, | ||||
|   0x91, 2, 0x0E, 0x09, | ||||
|   // Set VREG1A, VREG1B, VREG2A, VREG2B voltage | ||||
|   // According to datasheet set either 0xC3/0xC4 or 0xC9 only, but Lilygo sets both of them | ||||
|   0xC3, 5, 0x19, 0xC4, 0x19, 0xC9, 0x3C, | ||||
|   0xF0, 6, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3E,    // SET_GAMMA1 (F0h) | ||||
|   0xF1, 6, 0x56, 0xA8, 0x7F, 0x33, 0x34, 0x5F,    // SET_GAMMA2 (F1h) | ||||
|   0xF2, 6, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3A,    // SET_GAMMA3 (F2h) | ||||
|   0xF3, 6, 0x52, 0xA4, 0x7F, 0x33, 0x34, 0xDF,    // SET_GAMMA4 (F3h) | ||||
|   ILI9XXX_SLPOUT, 0,      // Sleep Out Mode (11h) | ||||
|   ILI9XXX_DELAY(10), | ||||
|   ILI9XXX_DISPON, 0,      // Display ON (29h) | ||||
|   ILI9XXX_DELAY(20), | ||||
|   0x00                    // End of list | ||||
| }; | ||||
|  | ||||
| static const uint8_t PROGMEM INITCMD_ST7735[] = { | ||||
|     ILI9XXX_SWRESET, 0,         // Soft reset, then delay 10ms | ||||
|     ILI9XXX_DELAY(10), | ||||
|   | ||||
							
								
								
									
										51
									
								
								esphome/components/ld2450/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								esphome/components/ld2450/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import uart | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_THROTTLE, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["uart"] | ||||
| CODEOWNERS = ["@hareeshmu"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| ld2450_ns = cg.esphome_ns.namespace("ld2450") | ||||
| LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice) | ||||
|  | ||||
| CONF_LD2450_ID = "ld2450_id" | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(LD2450Component), | ||||
|             cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All( | ||||
|                 cv.positive_time_period_milliseconds, | ||||
|                 cv.Range(min=cv.TimePeriod(milliseconds=1)), | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(uart.UART_DEVICE_SCHEMA) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
| LD2450BaseSchema = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), | ||||
|     }, | ||||
| ) | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( | ||||
|     "ld2450", | ||||
|     require_tx=True, | ||||
|     require_rx=True, | ||||
|     parity="NONE", | ||||
|     stop_bits=1, | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await uart.register_uart_device(var, config) | ||||
|     cg.add(var.set_throttle(config[CONF_THROTTLE])) | ||||
							
								
								
									
										47
									
								
								esphome/components/ld2450/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/ld2450/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import binary_sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_HAS_MOVING_TARGET, | ||||
|     CONF_HAS_STILL_TARGET, | ||||
|     CONF_HAS_TARGET, | ||||
|     DEVICE_CLASS_MOTION, | ||||
|     DEVICE_CLASS_OCCUPANCY, | ||||
| ) | ||||
|  | ||||
| from . import CONF_LD2450_ID, LD2450Component | ||||
|  | ||||
| DEPENDENCIES = ["ld2450"] | ||||
|  | ||||
| ICON_MEDITATION = "mdi:meditation" | ||||
| ICON_SHIELD_ACCOUNT = "mdi:shield-account" | ||||
| ICON_TARGET_ACCOUNT = "mdi:target-account" | ||||
|  | ||||
| CONFIG_SCHEMA = { | ||||
|     cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), | ||||
|     cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( | ||||
|         device_class=DEVICE_CLASS_OCCUPANCY, | ||||
|         icon=ICON_SHIELD_ACCOUNT, | ||||
|     ), | ||||
|     cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( | ||||
|         device_class=DEVICE_CLASS_MOTION, | ||||
|         icon=ICON_TARGET_ACCOUNT, | ||||
|     ), | ||||
|     cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( | ||||
|         device_class=DEVICE_CLASS_OCCUPANCY, | ||||
|         icon=ICON_MEDITATION, | ||||
|     ), | ||||
| } | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) | ||||
|     if has_target_config := config.get(CONF_HAS_TARGET): | ||||
|         sens = await binary_sensor.new_binary_sensor(has_target_config) | ||||
|         cg.add(ld2450_component.set_target_binary_sensor(sens)) | ||||
|     if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET): | ||||
|         sens = await binary_sensor.new_binary_sensor(has_moving_target_config) | ||||
|         cg.add(ld2450_component.set_moving_target_binary_sensor(sens)) | ||||
|     if has_still_target_config := config.get(CONF_HAS_STILL_TARGET): | ||||
|         sens = await binary_sensor.new_binary_sensor(has_still_target_config) | ||||
|         cg.add(ld2450_component.set_still_target_binary_sensor(sens)) | ||||
							
								
								
									
										45
									
								
								esphome/components/ld2450/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								esphome/components/ld2450/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import button | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_FACTORY_RESET, | ||||
|     CONF_RESTART, | ||||
|     DEVICE_CLASS_RESTART, | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     ICON_RESTART, | ||||
|     ICON_RESTART_ALERT, | ||||
| ) | ||||
|  | ||||
| from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns | ||||
|  | ||||
| ResetButton = ld2450_ns.class_("ResetButton", button.Button) | ||||
| RestartButton = ld2450_ns.class_("RestartButton", button.Button) | ||||
|  | ||||
| CONFIG_SCHEMA = { | ||||
|     cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), | ||||
|     cv.Optional(CONF_FACTORY_RESET): button.button_schema( | ||||
|         ResetButton, | ||||
|         device_class=DEVICE_CLASS_RESTART, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         icon=ICON_RESTART_ALERT, | ||||
|     ), | ||||
|     cv.Optional(CONF_RESTART): button.button_schema( | ||||
|         RestartButton, | ||||
|         device_class=DEVICE_CLASS_RESTART, | ||||
|         entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|         icon=ICON_RESTART, | ||||
|     ), | ||||
| } | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) | ||||
|     if factory_reset_config := config.get(CONF_FACTORY_RESET): | ||||
|         b = await button.new_button(factory_reset_config) | ||||
|         await cg.register_parented(b, config[CONF_LD2450_ID]) | ||||
|         cg.add(ld2450_component.set_reset_button(b)) | ||||
|     if restart_config := config.get(CONF_RESTART): | ||||
|         b = await button.new_button(restart_config) | ||||
|         await cg.register_parented(b, config[CONF_LD2450_ID]) | ||||
|         cg.add(ld2450_component.set_restart_button(b)) | ||||
							
								
								
									
										9
									
								
								esphome/components/ld2450/button/reset_button.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								esphome/components/ld2450/button/reset_button.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| #include "reset_button.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| void ResetButton::press_action() { this->parent_->factory_reset(); } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										18
									
								
								esphome/components/ld2450/button/reset_button.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/ld2450/button/reset_button.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/button/button.h" | ||||
| #include "../ld2450.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| class ResetButton : public button::Button, public Parented<LD2450Component> { | ||||
|  public: | ||||
|   ResetButton() = default; | ||||
|  | ||||
|  protected: | ||||
|   void press_action() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										9
									
								
								esphome/components/ld2450/button/restart_button.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								esphome/components/ld2450/button/restart_button.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| #include "restart_button.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										18
									
								
								esphome/components/ld2450/button/restart_button.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/ld2450/button/restart_button.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/button/button.h" | ||||
| #include "../ld2450.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| class RestartButton : public button::Button, public Parented<LD2450Component> { | ||||
|  public: | ||||
|   RestartButton() = default; | ||||
|  | ||||
|  protected: | ||||
|   void press_action() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										876
									
								
								esphome/components/ld2450/ld2450.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										876
									
								
								esphome/components/ld2450/ld2450.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,876 @@ | ||||
| #include "ld2450.h" | ||||
| #include <utility> | ||||
| #ifdef USE_NUMBER | ||||
| #include "esphome/components/number/number.h" | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #endif | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #define highbyte(val) (uint8_t)((val) >> 8) | ||||
| #define lowbyte(val) (uint8_t)((val) &0xff) | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| static const char *const TAG = "ld2450"; | ||||
| static const char *const UNKNOWN_MAC("unknown"); | ||||
|  | ||||
| // LD2450 UART Serial Commands | ||||
| static const uint8_t CMD_ENABLE_CONF = 0x00FF; | ||||
| static const uint8_t CMD_DISABLE_CONF = 0x00FE; | ||||
| static const uint8_t CMD_VERSION = 0x00A0; | ||||
| static const uint8_t CMD_MAC = 0x00A5; | ||||
| static const uint8_t CMD_RESET = 0x00A2; | ||||
| static const uint8_t CMD_RESTART = 0x00A3; | ||||
| static const uint8_t CMD_BLUETOOTH = 0x00A4; | ||||
| static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080; | ||||
| static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090; | ||||
| static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091; | ||||
| static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; | ||||
| static const uint8_t CMD_QUERY_ZONE = 0x00C1; | ||||
| static const uint8_t CMD_SET_ZONE = 0x00C2; | ||||
|  | ||||
| static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; | ||||
|  | ||||
| static inline std::string convert_signed_int_to_hex(int value) { | ||||
|   auto value_as_str = str_snprintf("%04x", 4, value & 0xFFFF); | ||||
|   return value_as_str; | ||||
| } | ||||
|  | ||||
| static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) { | ||||
|   for (int i = 0; i < 4; i++) { | ||||
|     std::string temp_hex = convert_signed_int_to_hex(values[i]); | ||||
|     bytes[i * 2] = std::stoi(temp_hex.substr(2, 2), nullptr, 16);      // Store high byte | ||||
|     bytes[i * 2 + 1] = std::stoi(temp_hex.substr(0, 2), nullptr, 16);  // Store low byte | ||||
|   } | ||||
| } | ||||
|  | ||||
| static inline int16_t decode_coordinate(uint8_t low_byte, uint8_t high_byte) { | ||||
|   int16_t coordinate = (high_byte & 0x7F) << 8 | low_byte; | ||||
|   if ((high_byte & 0x80) == 0) { | ||||
|     coordinate = -coordinate; | ||||
|   } | ||||
|   return coordinate;  // mm | ||||
| } | ||||
|  | ||||
| static inline int16_t decode_speed(uint8_t low_byte, uint8_t high_byte) { | ||||
|   int16_t speed = (high_byte & 0x7F) << 8 | low_byte; | ||||
|   if ((high_byte & 0x80) == 0) { | ||||
|     speed = -speed; | ||||
|   } | ||||
|   return speed * 10;  // mm/s | ||||
| } | ||||
|  | ||||
| static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) { | ||||
|   uint16_t hex_val = (buffer[offset + 1] << 8) | buffer[offset]; | ||||
|   int16_t dec_val = static_cast<int16_t>(hex_val); | ||||
|   if (dec_val & 0x8000) { | ||||
|     dec_val -= 65536; | ||||
|   } | ||||
|   return dec_val; | ||||
| } | ||||
|  | ||||
| static inline float calculate_angle(float base, float hypotenuse) { | ||||
|   if (base < 0.0 || hypotenuse <= 0.0) { | ||||
|     return 0.0; | ||||
|   } | ||||
|   float angle_radians = std::acos(base / hypotenuse); | ||||
|   float angle_degrees = angle_radians * (180.0 / M_PI); | ||||
|   return angle_degrees; | ||||
| } | ||||
|  | ||||
| static inline std::string get_direction(int16_t speed) { | ||||
|   static const char *const APPROACHING = "Approaching"; | ||||
|   static const char *const MOVING_AWAY = "Moving away"; | ||||
|   static const char *const STATIONARY = "Stationary"; | ||||
|  | ||||
|   if (speed > 0) { | ||||
|     return MOVING_AWAY; | ||||
|   } | ||||
|   if (speed < 0) { | ||||
|     return APPROACHING; | ||||
|   } | ||||
|   return STATIONARY; | ||||
| } | ||||
|  | ||||
| static inline std::string format_mac(uint8_t *buffer) { | ||||
|   return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], | ||||
|                       buffer[15]); | ||||
| } | ||||
|  | ||||
| static inline std::string format_version(uint8_t *buffer) { | ||||
|   return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], | ||||
|                      buffer[14]); | ||||
| } | ||||
|  | ||||
| LD2450Component::LD2450Component() {} | ||||
|  | ||||
| void LD2450Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up HLK-LD2450..."); | ||||
| #ifdef USE_NUMBER | ||||
|   if (this->presence_timeout_number_ != nullptr) { | ||||
|     this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_object_id_hash()); | ||||
|     this->set_presence_timeout(); | ||||
|   } | ||||
| #endif | ||||
|   this->restart_and_read_all_info(); | ||||
| } | ||||
|  | ||||
| void LD2450Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "HLK-LD2450 Human motion tracking radar module:"); | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   LOG_BINARY_SENSOR("  ", "TargetBinarySensor", this->target_binary_sensor_); | ||||
|   LOG_BINARY_SENSOR("  ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); | ||||
|   LOG_BINARY_SENSOR("  ", "StillTargetBinarySensor", this->still_target_binary_sensor_); | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
|   LOG_SWITCH("  ", "BluetoothSwitch", this->bluetooth_switch_); | ||||
|   LOG_SWITCH("  ", "MultiTargetSwitch", this->multi_target_switch_); | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|   LOG_BUTTON("  ", "ResetButton", this->reset_button_); | ||||
|   LOG_BUTTON("  ", "RestartButton", this->restart_button_); | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
|   LOG_SENSOR("  ", "TargetCountSensor", this->target_count_sensor_); | ||||
|   LOG_SENSOR("  ", "StillTargetCountSensor", this->still_target_count_sensor_); | ||||
|   LOG_SENSOR("  ", "MovingTargetCountSensor", this->moving_target_count_sensor_); | ||||
|   for (sensor::Sensor *s : this->move_x_sensors_) { | ||||
|     LOG_SENSOR("  ", "NthTargetXSensor", s); | ||||
|   } | ||||
|   for (sensor::Sensor *s : this->move_y_sensors_) { | ||||
|     LOG_SENSOR("  ", "NthTargetYSensor", s); | ||||
|   } | ||||
|   for (sensor::Sensor *s : this->move_speed_sensors_) { | ||||
|     LOG_SENSOR("  ", "NthTargetSpeedSensor", s); | ||||
|   } | ||||
|   for (sensor::Sensor *s : this->move_angle_sensors_) { | ||||
|     LOG_SENSOR("  ", "NthTargetAngleSensor", s); | ||||
|   } | ||||
|   for (sensor::Sensor *s : this->move_distance_sensors_) { | ||||
|     LOG_SENSOR("  ", "NthTargetDistanceSensor", s); | ||||
|   } | ||||
|   for (sensor::Sensor *s : this->move_resolution_sensors_) { | ||||
|     LOG_SENSOR("  ", "NthTargetResolutionSensor", s); | ||||
|   } | ||||
|   for (sensor::Sensor *s : this->zone_target_count_sensors_) { | ||||
|     LOG_SENSOR("  ", "NthZoneTargetCountSensor", s); | ||||
|   } | ||||
|   for (sensor::Sensor *s : this->zone_still_target_count_sensors_) { | ||||
|     LOG_SENSOR("  ", "NthZoneStillTargetCountSensor", s); | ||||
|   } | ||||
|   for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) { | ||||
|     LOG_SENSOR("  ", "NthZoneMovingTargetCountSensor", s); | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   LOG_TEXT_SENSOR("  ", "VersionTextSensor", this->version_text_sensor_); | ||||
|   LOG_TEXT_SENSOR("  ", "MacTextSensor", this->mac_text_sensor_); | ||||
|   for (text_sensor::TextSensor *s : this->direction_text_sensors_) { | ||||
|     LOG_TEXT_SENSOR("  ", "NthDirectionTextSensor", s); | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
|   for (auto n : this->zone_numbers_) { | ||||
|     LOG_NUMBER("  ", "ZoneX1Number", n.x1); | ||||
|     LOG_NUMBER("  ", "ZoneY1Number", n.y1); | ||||
|     LOG_NUMBER("  ", "ZoneX2Number", n.x2); | ||||
|     LOG_NUMBER("  ", "ZoneY2Number", n.y2); | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   LOG_SELECT("  ", "BaudRateSelect", this->baud_rate_select_); | ||||
|   LOG_SELECT("  ", "ZoneTypeSelect", this->zone_type_select_); | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
|   LOG_NUMBER("  ", "PresenceTimeoutNumber", this->presence_timeout_number_); | ||||
| #endif | ||||
|   ESP_LOGCONFIG(TAG, "  Throttle : %ums", this->throttle_); | ||||
|   ESP_LOGCONFIG(TAG, "  MAC Address : %s", const_cast<char *>(this->mac_.c_str())); | ||||
|   ESP_LOGCONFIG(TAG, "  Firmware version : %s", const_cast<char *>(this->version_.c_str())); | ||||
| } | ||||
|  | ||||
| void LD2450Component::loop() { | ||||
|   while (this->available()) { | ||||
|     this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Count targets in zone | ||||
| uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) { | ||||
|   uint8_t count = 0; | ||||
|   for (auto &index : this->target_info_) { | ||||
|     if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 && | ||||
|         index.is_moving == is_moving) { | ||||
|       count++; | ||||
|     } | ||||
|   } | ||||
|   return count; | ||||
| } | ||||
|  | ||||
| // Service reset_radar_zone | ||||
| void LD2450Component::reset_radar_zone() { | ||||
|   this->zone_type_ = 0; | ||||
|   for (auto &i : this->zone_config_) { | ||||
|     i.x1 = 0; | ||||
|     i.y1 = 0; | ||||
|     i.x2 = 0; | ||||
|     i.y2 = 0; | ||||
|   } | ||||
|   this->send_set_zone_command_(); | ||||
| } | ||||
|  | ||||
| void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, | ||||
|                                      int32_t zone1_y2, int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, | ||||
|                                      int32_t zone2_y2, int32_t zone3_x1, int32_t zone3_y1, int32_t zone3_x2, | ||||
|                                      int32_t zone3_y2) { | ||||
|   this->zone_type_ = zone_type; | ||||
|   int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1, | ||||
|                              zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2}; | ||||
|   for (int i = 0; i < MAX_ZONES; i++) { | ||||
|     this->zone_config_[i].x1 = zone_parameters[i * 4]; | ||||
|     this->zone_config_[i].y1 = zone_parameters[i * 4 + 1]; | ||||
|     this->zone_config_[i].x2 = zone_parameters[i * 4 + 2]; | ||||
|     this->zone_config_[i].y2 = zone_parameters[i * 4 + 3]; | ||||
|   } | ||||
|   this->send_set_zone_command_(); | ||||
| } | ||||
|  | ||||
| // Set Zone on LD2450 Sensor | ||||
| void LD2450Component::send_set_zone_command_() { | ||||
|   uint8_t cmd_value[26] = {}; | ||||
|   uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00}; | ||||
|   uint8_t area_config[24] = {}; | ||||
|   for (int i = 0; i < MAX_ZONES; i++) { | ||||
|     int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2, | ||||
|                      this->zone_config_[i].y2}; | ||||
|     ld2450::convert_int_values_to_hex(values, area_config + (i * 8)); | ||||
|   } | ||||
|   std::memcpy(cmd_value, zone_type_bytes, 2); | ||||
|   std::memcpy(cmd_value + 2, area_config, 24); | ||||
|   this->set_config_mode_(true); | ||||
|   this->send_command_(CMD_SET_ZONE, cmd_value, 26); | ||||
|   this->set_config_mode_(false); | ||||
| } | ||||
|  | ||||
| // Check presense timeout to reset presence status | ||||
| bool LD2450Component::get_timeout_status_(uint32_t check_millis) { | ||||
|   if (check_millis == 0) { | ||||
|     return true; | ||||
|   } | ||||
|   if (this->timeout_ == 0) { | ||||
|     this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT); | ||||
|   } | ||||
|   auto current_millis = millis(); | ||||
|   return current_millis - check_millis >= this->timeout_; | ||||
| } | ||||
|  | ||||
| // Extract, store and publish zone details LD2450 buffer | ||||
| void LD2450Component::process_zone_(uint8_t *buffer) { | ||||
|   uint8_t index, start; | ||||
|   for (index = 0; index < MAX_ZONES; index++) { | ||||
|     start = 12 + index * 8; | ||||
|     this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start); | ||||
|     this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2); | ||||
|     this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4); | ||||
|     this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6); | ||||
| #ifdef USE_NUMBER | ||||
|     // only one null check as all coordinates are required for a single zone | ||||
|     if (this->zone_numbers_[index].x1 != nullptr) { | ||||
|       this->zone_numbers_[index].x1->publish_state(this->zone_config_[index].x1); | ||||
|       this->zone_numbers_[index].y1->publish_state(this->zone_config_[index].y1); | ||||
|       this->zone_numbers_[index].x2->publish_state(this->zone_config_[index].x2); | ||||
|       this->zone_numbers_[index].y2->publish_state(this->zone_config_[index].y2); | ||||
|     } | ||||
| #endif | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Read all info from LD2450 buffer | ||||
| void LD2450Component::read_all_info() { | ||||
|   this->set_config_mode_(true); | ||||
|   this->get_version_(); | ||||
|   this->get_mac_(); | ||||
|   this->query_target_tracking_mode_(); | ||||
|   this->query_zone_(); | ||||
|   this->set_config_mode_(false); | ||||
| #ifdef USE_SELECT | ||||
|   const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); | ||||
|   if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) { | ||||
|     this->baud_rate_select_->publish_state(baud_rate); | ||||
|   } | ||||
|   this->publish_zone_type(); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| // Read zone info from LD2450 buffer | ||||
| void LD2450Component::query_zone_info() { | ||||
|   this->set_config_mode_(true); | ||||
|   this->query_zone_(); | ||||
|   this->set_config_mode_(false); | ||||
| } | ||||
|  | ||||
| // Restart LD2450 and read all info from buffer | ||||
| void LD2450Component::restart_and_read_all_info() { | ||||
|   this->set_config_mode_(true); | ||||
|   this->restart_(); | ||||
|   this->set_timeout(1500, [this]() { this->read_all_info(); }); | ||||
| } | ||||
|  | ||||
| // Send command with values to LD2450 | ||||
| void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { | ||||
|   ESP_LOGV(TAG, "Sending command %02X", command); | ||||
|   // frame header | ||||
|   this->write_array(CMD_FRAME_HEADER, 4); | ||||
|   // length bytes | ||||
|   int len = 2; | ||||
|   if (command_value != nullptr) { | ||||
|     len += command_value_len; | ||||
|   } | ||||
|   this->write_byte(lowbyte(len)); | ||||
|   this->write_byte(highbyte(len)); | ||||
|   // command | ||||
|   this->write_byte(lowbyte(command)); | ||||
|   this->write_byte(highbyte(command)); | ||||
|   // command value bytes | ||||
|   if (command_value != nullptr) { | ||||
|     for (int i = 0; i < command_value_len; i++) { | ||||
|       this->write_byte(command_value[i]); | ||||
|     } | ||||
|   } | ||||
|   // footer | ||||
|   this->write_array(CMD_FRAME_END, 4); | ||||
|   // FIXME to remove | ||||
|   delay(50);  // NOLINT | ||||
| } | ||||
|  | ||||
| // LD2450 Radar data message: | ||||
| //  [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC] | ||||
| //   Header       Target 1                  Target 2                  Target 3                  End | ||||
| void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | ||||
|   if (len < 29) {  // header (4 bytes) + 8 x 3 target data + footer (2 bytes) | ||||
|     ESP_LOGE(TAG, "Periodic data: invalid message length"); | ||||
|     return; | ||||
|   } | ||||
|   if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) {  // header | ||||
|     ESP_LOGE(TAG, "Periodic data: invalid message header"); | ||||
|     return; | ||||
|   } | ||||
|   if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) {  // footer | ||||
|     ESP_LOGE(TAG, "Periodic data: invalid message footer"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   auto current_millis = millis(); | ||||
|   if (current_millis - this->last_periodic_millis_ < this->throttle_) { | ||||
|     ESP_LOGV(TAG, "Throttling: %d", this->throttle_); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->last_periodic_millis_ = current_millis; | ||||
|  | ||||
|   int16_t target_count = 0; | ||||
|   int16_t still_target_count = 0; | ||||
|   int16_t moving_target_count = 0; | ||||
|   int16_t start = 0; | ||||
|   int16_t val = 0; | ||||
|   uint8_t index = 0; | ||||
|   int16_t tx = 0; | ||||
|   int16_t ty = 0; | ||||
|   int16_t td = 0; | ||||
|   int16_t ts = 0; | ||||
|   int16_t angle = 0; | ||||
|   std::string direction{}; | ||||
|   bool is_moving = false; | ||||
|  | ||||
| #if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR) | ||||
|   // Loop thru targets | ||||
|   for (index = 0; index < MAX_TARGETS; index++) { | ||||
| #ifdef USE_SENSOR | ||||
|     // X | ||||
|     start = TARGET_X + index * 8; | ||||
|     is_moving = false; | ||||
|     sensor::Sensor *sx = this->move_x_sensors_[index]; | ||||
|     if (sx != nullptr) { | ||||
|       val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); | ||||
|       tx = val; | ||||
|       sx->publish_state(val); | ||||
|     } | ||||
|     // Y | ||||
|     start = TARGET_Y + index * 8; | ||||
|     sensor::Sensor *sy = this->move_y_sensors_[index]; | ||||
|     if (sy != nullptr) { | ||||
|       val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); | ||||
|       ty = val; | ||||
|       sy->publish_state(val); | ||||
|     } | ||||
|     // RESOLUTION | ||||
|     start = TARGET_RESOLUTION + index * 8; | ||||
|     sensor::Sensor *sr = this->move_resolution_sensors_[index]; | ||||
|     if (sr != nullptr) { | ||||
|       val = (buffer[start + 1] << 8) | buffer[start]; | ||||
|       sr->publish_state(val); | ||||
|     } | ||||
| #endif | ||||
|     // SPEED | ||||
|     start = TARGET_SPEED + index * 8; | ||||
|     val = ld2450::decode_speed(buffer[start], buffer[start + 1]); | ||||
|     ts = val; | ||||
|     if (val) { | ||||
|       is_moving = true; | ||||
|       moving_target_count++; | ||||
|     } | ||||
| #ifdef USE_SENSOR | ||||
|     sensor::Sensor *ss = this->move_speed_sensors_[index]; | ||||
|     if (ss != nullptr) { | ||||
|       ss->publish_state(val); | ||||
|     } | ||||
| #endif | ||||
|     // DISTANCE | ||||
|     val = (uint16_t) sqrt( | ||||
|         pow(ld2450::decode_coordinate(buffer[TARGET_X + index * 8], buffer[(TARGET_X + index * 8) + 1]), 2) + | ||||
|         pow(ld2450::decode_coordinate(buffer[TARGET_Y + index * 8], buffer[(TARGET_Y + index * 8) + 1]), 2)); | ||||
|     td = val; | ||||
|     if (val > 0) { | ||||
|       target_count++; | ||||
|     } | ||||
| #ifdef USE_SENSOR | ||||
|     sensor::Sensor *sd = this->move_distance_sensors_[index]; | ||||
|     if (sd != nullptr) { | ||||
|       sd->publish_state(val); | ||||
|     } | ||||
|     // ANGLE | ||||
|     angle = calculate_angle(static_cast<float>(ty), static_cast<float>(td)); | ||||
|     if (tx > 0) { | ||||
|       angle = angle * -1; | ||||
|     } | ||||
|     sensor::Sensor *sa = this->move_angle_sensors_[index]; | ||||
|     if (sa != nullptr) { | ||||
|       sa->publish_state(angle); | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|     // DIRECTION | ||||
|     direction = get_direction(ts); | ||||
|     if (td == 0) { | ||||
|       direction = "NA"; | ||||
|     } | ||||
|     text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; | ||||
|     if (tsd != nullptr) { | ||||
|       tsd->publish_state(direction); | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     // Store target info for zone target count | ||||
|     this->target_info_[index].x = tx; | ||||
|     this->target_info_[index].y = ty; | ||||
|     this->target_info_[index].is_moving = is_moving; | ||||
|  | ||||
|   }  // End loop thru targets | ||||
|  | ||||
|   still_target_count = target_count - moving_target_count; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
|   // Loop thru zones | ||||
|   uint8_t zone_still_targets = 0; | ||||
|   uint8_t zone_moving_targets = 0; | ||||
|   uint8_t zone_all_targets = 0; | ||||
|   for (index = 0; index < MAX_ZONES; index++) { | ||||
|     zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false); | ||||
|     zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true); | ||||
|     zone_all_targets = zone_still_targets + zone_moving_targets; | ||||
|  | ||||
|     // Publish Still Target Count in Zones | ||||
|     sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index]; | ||||
|     if (szstc != nullptr) { | ||||
|       szstc->publish_state(zone_still_targets); | ||||
|     } | ||||
|     // Publish Moving Target Count in Zones | ||||
|     sensor::Sensor *szmtc = this->zone_moving_target_count_sensors_[index]; | ||||
|     if (szmtc != nullptr) { | ||||
|       szmtc->publish_state(zone_moving_targets); | ||||
|     } | ||||
|     // Publish All Target Count in Zones | ||||
|     sensor::Sensor *sztc = this->zone_target_count_sensors_[index]; | ||||
|     if (sztc != nullptr) { | ||||
|       sztc->publish_state(zone_all_targets); | ||||
|     } | ||||
|  | ||||
|   }  // End loop thru zones | ||||
|  | ||||
|   // Target Count | ||||
|   if (this->target_count_sensor_ != nullptr) { | ||||
|     this->target_count_sensor_->publish_state(target_count); | ||||
|   } | ||||
|   // Still Target Count | ||||
|   if (this->still_target_count_sensor_ != nullptr) { | ||||
|     this->still_target_count_sensor_->publish_state(still_target_count); | ||||
|   } | ||||
|   // Moving Target Count | ||||
|   if (this->moving_target_count_sensor_ != nullptr) { | ||||
|     this->moving_target_count_sensor_->publish_state(moving_target_count); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   // Target Presence | ||||
|   if (this->target_binary_sensor_ != nullptr) { | ||||
|     if (target_count > 0) { | ||||
|       this->target_binary_sensor_->publish_state(true); | ||||
|     } else { | ||||
|       if (this->get_timeout_status_(this->presence_millis_)) { | ||||
|         this->target_binary_sensor_->publish_state(false); | ||||
|       } else { | ||||
|         ESP_LOGV(TAG, "Clear presence waiting timeout: %d", this->timeout_); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   // Moving Target Presence | ||||
|   if (this->moving_target_binary_sensor_ != nullptr) { | ||||
|     if (moving_target_count > 0) { | ||||
|       this->moving_target_binary_sensor_->publish_state(true); | ||||
|     } else { | ||||
|       if (this->get_timeout_status_(this->moving_presence_millis_)) { | ||||
|         this->moving_target_binary_sensor_->publish_state(false); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   // Still Target Presence | ||||
|   if (this->still_target_binary_sensor_ != nullptr) { | ||||
|     if (still_target_count > 0) { | ||||
|       this->still_target_binary_sensor_->publish_state(true); | ||||
|     } else { | ||||
|       if (this->get_timeout_status_(this->still_presence_millis_)) { | ||||
|         this->still_target_binary_sensor_->publish_state(false); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
|   // For presence timeout check | ||||
|   if (target_count > 0) { | ||||
|     this->presence_millis_ = millis(); | ||||
|   } | ||||
|   if (moving_target_count > 0) { | ||||
|     this->moving_presence_millis_ = millis(); | ||||
|   } | ||||
|   if (still_target_count > 0) { | ||||
|     this->still_presence_millis_ = millis(); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
|   ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]); | ||||
|   if (len < 10) { | ||||
|     ESP_LOGE(TAG, "Ack data: invalid length"); | ||||
|     return true; | ||||
|   } | ||||
|   if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // frame header | ||||
|     ESP_LOGE(TAG, "Ack data: invalid header (command %02X)", buffer[COMMAND]); | ||||
|     return true; | ||||
|   } | ||||
|   if (buffer[COMMAND_STATUS] != 0x01) { | ||||
|     ESP_LOGE(TAG, "Ack data: invalid status"); | ||||
|     return true; | ||||
|   } | ||||
|   if (buffer[8] || buffer[9]) { | ||||
|     ESP_LOGE(TAG, "Ack data: last buffer was %u, %u", buffer[8], buffer[9]); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   switch (buffer[COMMAND]) { | ||||
|     case lowbyte(CMD_ENABLE_CONF): | ||||
|       ESP_LOGV(TAG, "Got enable conf command"); | ||||
|       break; | ||||
|     case lowbyte(CMD_DISABLE_CONF): | ||||
|       ESP_LOGV(TAG, "Got disable conf command"); | ||||
|       break; | ||||
|     case lowbyte(CMD_SET_BAUD_RATE): | ||||
|       ESP_LOGV(TAG, "Got baud rate change command"); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->baud_rate_select_ != nullptr) { | ||||
|         ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str()); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_VERSION): | ||||
|       this->version_ = ld2450::format_version(buffer); | ||||
|       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|       if (this->version_text_sensor_ != nullptr) { | ||||
|         this->version_text_sensor_->publish_state(this->version_); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_MAC): | ||||
|       if (len < 20) { | ||||
|         return false; | ||||
|       } | ||||
|       this->mac_ = ld2450::format_mac(buffer); | ||||
|       ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|       if (this->mac_text_sensor_ != nullptr) { | ||||
|         this->mac_text_sensor_->publish_state(this->mac_); | ||||
|       } | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->bluetooth_switch_ != nullptr) { | ||||
|         this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_BLUETOOTH): | ||||
|       ESP_LOGV(TAG, "Got Bluetooth command"); | ||||
|       break; | ||||
|     case lowbyte(CMD_SINGLE_TARGET_MODE): | ||||
|       ESP_LOGV(TAG, "Got single target conf command"); | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->multi_target_switch_ != nullptr) { | ||||
|         this->multi_target_switch_->publish_state(false); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_MULTI_TARGET_MODE): | ||||
|       ESP_LOGV(TAG, "Got multi target conf command"); | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->multi_target_switch_ != nullptr) { | ||||
|         this->multi_target_switch_->publish_state(true); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_QUERY_TARGET_MODE): | ||||
|       ESP_LOGV(TAG, "Got query target tracking mode command"); | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->multi_target_switch_ != nullptr) { | ||||
|         this->multi_target_switch_->publish_state(buffer[10] == 0x02); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_QUERY_ZONE): | ||||
|       ESP_LOGV(TAG, "Got query zone conf command"); | ||||
|       this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16); | ||||
|       this->publish_zone_type(); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->zone_type_select_ != nullptr) { | ||||
|         ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); | ||||
|       } | ||||
| #endif | ||||
|       if (buffer[10] == 0x00) { | ||||
|         ESP_LOGV(TAG, "Zone: Disabled"); | ||||
|       } | ||||
|       if (buffer[10] == 0x01) { | ||||
|         ESP_LOGV(TAG, "Zone: Area detection"); | ||||
|       } | ||||
|       if (buffer[10] == 0x02) { | ||||
|         ESP_LOGV(TAG, "Zone: Area filter"); | ||||
|       } | ||||
|       this->process_zone_(buffer); | ||||
|       break; | ||||
|     case lowbyte(CMD_SET_ZONE): | ||||
|       ESP_LOGV(TAG, "Got set zone conf command"); | ||||
|       this->query_zone_info(); | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| // Read LD2450 buffer data | ||||
| void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) { | ||||
|   if (readch < 0) { | ||||
|     return; | ||||
|   } | ||||
|   if (this->buffer_pos_ < len - 1) { | ||||
|     buffer[this->buffer_pos_++] = readch; | ||||
|     buffer[this->buffer_pos_] = 0; | ||||
|   } else { | ||||
|     this->buffer_pos_ = 0; | ||||
|   } | ||||
|   if (this->buffer_pos_ < 4) { | ||||
|     return; | ||||
|   } | ||||
|   if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) { | ||||
|     ESP_LOGV(TAG, "Handle periodic radar data"); | ||||
|     this->handle_periodic_data_(buffer, this->buffer_pos_); | ||||
|     this->buffer_pos_ = 0;  // Reset position index for next frame | ||||
|   } else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 && | ||||
|              buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) { | ||||
|     ESP_LOGV(TAG, "Handle command ack data"); | ||||
|     if (this->handle_ack_data_(buffer, this->buffer_pos_)) { | ||||
|       this->buffer_pos_ = 0;  // Reset position index for next frame | ||||
|     } else { | ||||
|       ESP_LOGV(TAG, "Command ack data invalid"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Set Config Mode - Pre-requisite sending commands | ||||
| void LD2450Component::set_config_mode_(bool enable) { | ||||
|   uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; | ||||
|   uint8_t cmd_value[2] = {0x01, 0x00}; | ||||
|   this->send_command_(cmd, enable ? cmd_value : nullptr, 2); | ||||
| } | ||||
|  | ||||
| // Set Bluetooth Enable/Disable | ||||
| void LD2450Component::set_bluetooth(bool enable) { | ||||
|   this->set_config_mode_(true); | ||||
|   uint8_t enable_cmd_value[2] = {0x01, 0x00}; | ||||
|   uint8_t disable_cmd_value[2] = {0x00, 0x00}; | ||||
|   this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2); | ||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
| } | ||||
|  | ||||
| // Set Baud rate | ||||
| void LD2450Component::set_baud_rate(const std::string &state) { | ||||
|   this->set_config_mode_(true); | ||||
|   uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; | ||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); | ||||
|   this->set_timeout(200, [this]() { this->restart_(); }); | ||||
| } | ||||
|  | ||||
| // Set Zone Type - one of: Disabled, Detection, Filter | ||||
| void LD2450Component::set_zone_type(const std::string &state) { | ||||
|   ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); | ||||
|   uint8_t zone_type = ZONE_TYPE_ENUM_TO_INT.at(state); | ||||
|   this->zone_type_ = zone_type; | ||||
|   this->send_set_zone_command_(); | ||||
| } | ||||
|  | ||||
| // Publish Zone Type to Select component | ||||
| void LD2450Component::publish_zone_type() { | ||||
| #ifdef USE_SELECT | ||||
|   std::string zone_type = ZONE_TYPE_INT_TO_ENUM.at(static_cast<ZoneTypeStructure>(this->zone_type_)); | ||||
|   if (this->zone_type_select_ != nullptr) { | ||||
|     this->zone_type_select_->publish_state(zone_type); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| // Set Single/Multiplayer target detection | ||||
| void LD2450Component::set_multi_target(bool enable) { | ||||
|   this->set_config_mode_(true); | ||||
|   uint8_t cmd = enable ? CMD_MULTI_TARGET_MODE : CMD_SINGLE_TARGET_MODE; | ||||
|   this->send_command_(cmd, nullptr, 0); | ||||
|   this->set_config_mode_(false); | ||||
| } | ||||
|  | ||||
| // LD2450 factory reset | ||||
| void LD2450Component::factory_reset() { | ||||
|   this->set_config_mode_(true); | ||||
|   this->send_command_(CMD_RESET, nullptr, 0); | ||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
| } | ||||
|  | ||||
| // Restart LD2450 module | ||||
| void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } | ||||
|  | ||||
| // Get LD2450 firmware version | ||||
| void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } | ||||
|  | ||||
| // Get LD2450 mac address | ||||
| void LD2450Component::get_mac_() { | ||||
|   uint8_t cmd_value[2] = {0x01, 0x00}; | ||||
|   this->send_command_(CMD_MAC, cmd_value, 2); | ||||
| } | ||||
|  | ||||
| // Query for target tracking mode | ||||
| void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QUERY_TARGET_MODE, nullptr, 0); } | ||||
|  | ||||
| // Query for zone info | ||||
| void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); } | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
| void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) { this->move_x_sensors_[target] = s; } | ||||
| void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) { this->move_y_sensors_[target] = s; } | ||||
| void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) { | ||||
|   this->move_speed_sensors_[target] = s; | ||||
| } | ||||
| void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) { | ||||
|   this->move_angle_sensors_[target] = s; | ||||
| } | ||||
| void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) { | ||||
|   this->move_distance_sensors_[target] = s; | ||||
| } | ||||
| void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) { | ||||
|   this->move_resolution_sensors_[target] = s; | ||||
| } | ||||
| void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) { | ||||
|   this->zone_target_count_sensors_[zone] = s; | ||||
| } | ||||
| void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) { | ||||
|   this->zone_still_target_count_sensors_[zone] = s; | ||||
| } | ||||
| void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) { | ||||
|   this->zone_moving_target_count_sensors_[zone] = s; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
| void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s) { | ||||
|   this->direction_text_sensors_[target] = s; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| // Send Zone coordinates data to LD2450 | ||||
| #ifdef USE_NUMBER | ||||
| void LD2450Component::set_zone_coordinate(uint8_t zone) { | ||||
|   number::Number *x1sens = this->zone_numbers_[zone].x1; | ||||
|   number::Number *y1sens = this->zone_numbers_[zone].y1; | ||||
|   number::Number *x2sens = this->zone_numbers_[zone].x2; | ||||
|   number::Number *y2sens = this->zone_numbers_[zone].y2; | ||||
|   if (!x1sens->has_state() || !y1sens->has_state() || !x2sens->has_state() || !y2sens->has_state()) { | ||||
|     return; | ||||
|   } | ||||
|   this->zone_config_[zone].x1 = static_cast<int>(x1sens->state); | ||||
|   this->zone_config_[zone].y1 = static_cast<int>(y1sens->state); | ||||
|   this->zone_config_[zone].x2 = static_cast<int>(x2sens->state); | ||||
|   this->zone_config_[zone].y2 = static_cast<int>(y2sens->state); | ||||
|   this->send_set_zone_command_(); | ||||
| } | ||||
|  | ||||
| void LD2450Component::set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2, | ||||
|                                        number::Number *y2) { | ||||
|   if (zone < MAX_ZONES) { | ||||
|     this->zone_numbers_[zone].x1 = x1; | ||||
|     this->zone_numbers_[zone].y1 = y1; | ||||
|     this->zone_numbers_[zone].x2 = x2; | ||||
|     this->zone_numbers_[zone].y2 = y2; | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| // Set Presence Timeout load and save from flash | ||||
| #ifdef USE_NUMBER | ||||
| void LD2450Component::set_presence_timeout() { | ||||
|   if (this->presence_timeout_number_ != nullptr) { | ||||
|     if (this->presence_timeout_number_->state == 0) { | ||||
|       float timeout = this->restore_from_flash_(); | ||||
|       this->presence_timeout_number_->publish_state(timeout); | ||||
|       this->timeout_ = ld2450::convert_seconds_to_ms(timeout); | ||||
|     } | ||||
|     if (this->presence_timeout_number_->has_state()) { | ||||
|       this->save_to_flash_(this->presence_timeout_number_->state); | ||||
|       this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Save Presence Timeout to flash | ||||
| void LD2450Component::save_to_flash_(float value) { this->pref_.save(&value); } | ||||
|  | ||||
| // Load Presence Timeout from flash | ||||
| float LD2450Component::restore_from_flash_() { | ||||
|   float value; | ||||
|   if (!this->pref_.load(&value)) { | ||||
|     value = DEFAULT_PRESENCE_TIMEOUT; | ||||
|   } | ||||
|   return value; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										234
									
								
								esphome/components/ld2450/ld2450.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								esphome/components/ld2450/ld2450.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,234 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <iomanip> | ||||
| #include <map> | ||||
| #include "esphome/components/uart/uart.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #ifdef USE_SENSOR | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
| #include "esphome/components/number/number.h" | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
| #include "esphome/components/switch/switch.h" | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
| #include "esphome/components/button/button.h" | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
| #include "esphome/components/select/select.h" | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
| #include "esphome/components/text_sensor/text_sensor.h" | ||||
| #endif | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
| #endif | ||||
|  | ||||
| #ifndef M_PI | ||||
| #define M_PI 3.14 | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| // Constants | ||||
| static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5;  // Timeout to reset presense status 5 sec. | ||||
| static const uint8_t MAX_LINE_LENGTH = 60;          // Max characters for serial buffer | ||||
| static const uint8_t MAX_TARGETS = 3;               // Max 3 Targets in LD2450 | ||||
| static const uint8_t MAX_ZONES = 3;                 // Max 3 Zones in LD2450 | ||||
|  | ||||
| // Target coordinate struct | ||||
| struct Target { | ||||
|   int16_t x; | ||||
|   int16_t y; | ||||
|   bool is_moving; | ||||
| }; | ||||
|  | ||||
| // Zone coordinate struct | ||||
| struct Zone { | ||||
|   int16_t x1 = 0; | ||||
|   int16_t y1 = 0; | ||||
|   int16_t x2 = 0; | ||||
|   int16_t y2 = 0; | ||||
| }; | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
| struct ZoneOfNumbers { | ||||
|   number::Number *x1 = nullptr; | ||||
|   number::Number *y1 = nullptr; | ||||
|   number::Number *x2 = nullptr; | ||||
|   number::Number *y2 = nullptr; | ||||
| }; | ||||
| #endif | ||||
|  | ||||
| enum BaudRateStructure : uint8_t { | ||||
|   BAUD_RATE_9600 = 1, | ||||
|   BAUD_RATE_19200 = 2, | ||||
|   BAUD_RATE_38400 = 3, | ||||
|   BAUD_RATE_57600 = 4, | ||||
|   BAUD_RATE_115200 = 5, | ||||
|   BAUD_RATE_230400 = 6, | ||||
|   BAUD_RATE_256000 = 7, | ||||
|   BAUD_RATE_460800 = 8 | ||||
| }; | ||||
|  | ||||
| // Convert baud rate enum to int | ||||
| static const std::map<std::string, uint8_t> BAUD_RATE_ENUM_TO_INT{ | ||||
|     {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400}, | ||||
|     {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, | ||||
|     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; | ||||
|  | ||||
| // Zone type struct | ||||
| enum ZoneTypeStructure : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2 }; | ||||
|  | ||||
| // Convert zone type int to enum | ||||
| static const std::map<ZoneTypeStructure, std::string> ZONE_TYPE_INT_TO_ENUM{ | ||||
|     {ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, {ZONE_FILTER, "Filter"}}; | ||||
|  | ||||
| // Convert zone type enum to int | ||||
| static const std::map<std::string, uint8_t> ZONE_TYPE_ENUM_TO_INT{ | ||||
|     {"Disabled", ZONE_DISABLED}, {"Detection", ZONE_DETECTION}, {"Filter", ZONE_FILTER}}; | ||||
|  | ||||
| // LD2450 serial command header & footer | ||||
| static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; | ||||
| static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; | ||||
|  | ||||
| enum PeriodicDataStructure : uint8_t { | ||||
|   TARGET_X = 4, | ||||
|   TARGET_Y = 6, | ||||
|   TARGET_SPEED = 8, | ||||
|   TARGET_RESOLUTION = 10, | ||||
| }; | ||||
|  | ||||
| enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 }; | ||||
|  | ||||
| enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; | ||||
|  | ||||
| class LD2450Component : public Component, public uart::UARTDevice { | ||||
| #ifdef USE_SENSOR | ||||
|   SUB_SENSOR(target_count) | ||||
|   SUB_SENSOR(still_target_count) | ||||
|   SUB_SENSOR(moving_target_count) | ||||
| #endif | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   SUB_BINARY_SENSOR(target) | ||||
|   SUB_BINARY_SENSOR(moving_target) | ||||
|   SUB_BINARY_SENSOR(still_target) | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   SUB_TEXT_SENSOR(version) | ||||
|   SUB_TEXT_SENSOR(mac) | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   SUB_SELECT(baud_rate) | ||||
|   SUB_SELECT(zone_type) | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
|   SUB_SWITCH(bluetooth) | ||||
|   SUB_SWITCH(multi_target) | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|   SUB_BUTTON(reset) | ||||
|   SUB_BUTTON(restart) | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
|   SUB_NUMBER(presence_timeout) | ||||
| #endif | ||||
|  | ||||
|  public: | ||||
|   LD2450Component(); | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
|   void set_presence_timeout(); | ||||
|   void set_throttle(uint16_t value) { this->throttle_ = value; }; | ||||
|   void read_all_info(); | ||||
|   void query_zone_info(); | ||||
|   void restart_and_read_all_info(); | ||||
|   void set_bluetooth(bool enable); | ||||
|   void set_multi_target(bool enable); | ||||
|   void set_baud_rate(const std::string &state); | ||||
|   void set_zone_type(const std::string &state); | ||||
|   void publish_zone_type(); | ||||
|   void factory_reset(); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   void set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s); | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
|   void set_zone_coordinate(uint8_t zone); | ||||
|   void set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2, number::Number *y2); | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
|   void set_move_x_sensor(uint8_t target, sensor::Sensor *s); | ||||
|   void set_move_y_sensor(uint8_t target, sensor::Sensor *s); | ||||
|   void set_move_speed_sensor(uint8_t target, sensor::Sensor *s); | ||||
|   void set_move_angle_sensor(uint8_t target, sensor::Sensor *s); | ||||
|   void set_move_distance_sensor(uint8_t target, sensor::Sensor *s); | ||||
|   void set_move_resolution_sensor(uint8_t target, sensor::Sensor *s); | ||||
|   void set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s); | ||||
|   void set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s); | ||||
|   void set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s); | ||||
| #endif | ||||
|   void reset_radar_zone(); | ||||
|   void set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, int32_t zone1_y2, | ||||
|                       int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1, | ||||
|                       int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2); | ||||
|  | ||||
|  protected: | ||||
|   void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); | ||||
|   void set_config_mode_(bool enable); | ||||
|   void handle_periodic_data_(uint8_t *buffer, uint8_t len); | ||||
|   bool handle_ack_data_(uint8_t *buffer, uint8_t len); | ||||
|   void process_zone_(uint8_t *buffer); | ||||
|   void readline_(int readch, uint8_t *buffer, uint8_t len); | ||||
|   void get_version_(); | ||||
|   void get_mac_(); | ||||
|   void query_target_tracking_mode_(); | ||||
|   void query_zone_(); | ||||
|   void restart_(); | ||||
|   void send_set_zone_command_(); | ||||
|   void save_to_flash_(float value); | ||||
|   float restore_from_flash_(); | ||||
|   bool get_timeout_status_(uint32_t check_millis); | ||||
|   uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); | ||||
|  | ||||
|   Target target_info_[MAX_TARGETS]; | ||||
|   Zone zone_config_[MAX_ZONES]; | ||||
|   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer | ||||
|   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||
|   uint32_t last_periodic_millis_ = 0; | ||||
|   uint32_t presence_millis_ = 0; | ||||
|   uint32_t still_presence_millis_ = 0; | ||||
|   uint32_t moving_presence_millis_ = 0; | ||||
|   uint16_t throttle_ = 0; | ||||
|   uint16_t timeout_ = 5; | ||||
|   uint8_t zone_type_ = 0; | ||||
|   std::string version_{}; | ||||
|   std::string mac_{}; | ||||
| #ifdef USE_NUMBER | ||||
|   ESPPreferenceObject pref_;  // only used when numbers are in use | ||||
|   ZoneOfNumbers zone_numbers_[MAX_ZONES]; | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
|   std::vector<sensor::Sensor *> move_x_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS); | ||||
|   std::vector<sensor::Sensor *> move_y_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS); | ||||
|   std::vector<sensor::Sensor *> move_speed_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS); | ||||
|   std::vector<sensor::Sensor *> move_angle_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS); | ||||
|   std::vector<sensor::Sensor *> move_distance_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS); | ||||
|   std::vector<sensor::Sensor *> move_resolution_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS); | ||||
|   std::vector<sensor::Sensor *> zone_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES); | ||||
|   std::vector<sensor::Sensor *> zone_still_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES); | ||||
|   std::vector<sensor::Sensor *> zone_moving_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES); | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   std::vector<text_sensor::TextSensor *> direction_text_sensors_ = std::vector<text_sensor::TextSensor *>(3); | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										121
									
								
								esphome/components/ld2450/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/components/ld2450/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import number | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     DEVICE_CLASS_DISTANCE, | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
|     ICON_TIMELAPSE, | ||||
|     UNIT_MILLIMETER, | ||||
|     UNIT_SECOND, | ||||
| ) | ||||
|  | ||||
| from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns | ||||
|  | ||||
| CONF_PRESENCE_TIMEOUT = "presence_timeout" | ||||
| CONF_X1 = "x1" | ||||
| CONF_X2 = "x2" | ||||
| CONF_Y1 = "y1" | ||||
| CONF_Y2 = "y2" | ||||
| ICON_ARROW_BOTTOM_RIGHT = "mdi:arrow-bottom-right" | ||||
| ICON_ARROW_BOTTOM_RIGHT_BOLD_BOX_OUTLINE = "mdi:arrow-bottom-right-bold-box-outline" | ||||
| ICON_ARROW_TOP_LEFT = "mdi:arrow-top-left" | ||||
| ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE = "mdi:arrow-top-left-bold-box-outline" | ||||
| MAX_ZONES = 3 | ||||
|  | ||||
| PresenceTimeoutNumber = ld2450_ns.class_("PresenceTimeoutNumber", number.Number) | ||||
| ZoneCoordinateNumber = ld2450_ns.class_("ZoneCoordinateNumber", number.Number) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), | ||||
|         cv.Required(CONF_PRESENCE_TIMEOUT): number.number_schema( | ||||
|             PresenceTimeoutNumber, | ||||
|             unit_of_measurement=UNIT_SECOND, | ||||
|             entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|             icon=ICON_TIMELAPSE, | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = CONFIG_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(f"zone_{n + 1}"): cv.Schema( | ||||
|             { | ||||
|                 cv.Required(CONF_X1): number.number_schema( | ||||
|                     ZoneCoordinateNumber, | ||||
|                     device_class=DEVICE_CLASS_DISTANCE, | ||||
|                     unit_of_measurement=UNIT_MILLIMETER, | ||||
|                     entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|                     icon=ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE, | ||||
|                 ), | ||||
|                 cv.Required(CONF_Y1): number.number_schema( | ||||
|                     ZoneCoordinateNumber, | ||||
|                     device_class=DEVICE_CLASS_DISTANCE, | ||||
|                     unit_of_measurement=UNIT_MILLIMETER, | ||||
|                     entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|                     icon=ICON_ARROW_TOP_LEFT, | ||||
|                 ), | ||||
|                 cv.Required(CONF_X2): number.number_schema( | ||||
|                     ZoneCoordinateNumber, | ||||
|                     device_class=DEVICE_CLASS_DISTANCE, | ||||
|                     unit_of_measurement=UNIT_MILLIMETER, | ||||
|                     entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|                     icon=ICON_ARROW_BOTTOM_RIGHT_BOLD_BOX_OUTLINE, | ||||
|                 ), | ||||
|                 cv.Required(CONF_Y2): number.number_schema( | ||||
|                     ZoneCoordinateNumber, | ||||
|                     device_class=DEVICE_CLASS_DISTANCE, | ||||
|                     unit_of_measurement=UNIT_MILLIMETER, | ||||
|                     entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|                     icon=ICON_ARROW_BOTTOM_RIGHT, | ||||
|                 ), | ||||
|             } | ||||
|         ) | ||||
|         for n in range(MAX_ZONES) | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) | ||||
|     if presence_timeout_config := config.get(CONF_PRESENCE_TIMEOUT): | ||||
|         n = await number.new_number( | ||||
|             presence_timeout_config, | ||||
|             min_value=0, | ||||
|             max_value=3600, | ||||
|             step=1, | ||||
|         ) | ||||
|         await cg.register_parented(n, config[CONF_LD2450_ID]) | ||||
|         cg.add(ld2450_component.set_presence_timeout_number(n)) | ||||
|     for zone_num in range(MAX_ZONES): | ||||
|         if zone_conf := config.get(f"zone_{zone_num + 1}"): | ||||
|             zone_x1_config = zone_conf.get(CONF_X1) | ||||
|             x1 = cg.new_Pvariable(zone_x1_config[CONF_ID], zone_num) | ||||
|             await number.register_number( | ||||
|                 x1, zone_x1_config, min_value=-4860, max_value=4860, step=1 | ||||
|             ) | ||||
|             await cg.register_parented(x1, config[CONF_LD2450_ID]) | ||||
|  | ||||
|             zone_y1_config = zone_conf.get(CONF_Y1) | ||||
|             y1 = cg.new_Pvariable(zone_y1_config[CONF_ID], zone_num) | ||||
|             await number.register_number( | ||||
|                 y1, zone_y1_config, min_value=0, max_value=7560, step=1 | ||||
|             ) | ||||
|             await cg.register_parented(y1, config[CONF_LD2450_ID]) | ||||
|  | ||||
|             zone_x2_config = zone_conf.get(CONF_X2) | ||||
|             x2 = cg.new_Pvariable(zone_x2_config[CONF_ID], zone_num) | ||||
|             await number.register_number( | ||||
|                 x2, zone_x2_config, min_value=-4860, max_value=4860, step=1 | ||||
|             ) | ||||
|             await cg.register_parented(x2, config[CONF_LD2450_ID]) | ||||
|  | ||||
|             zone_y2_config = zone_conf.get(CONF_Y2) | ||||
|             y2 = cg.new_Pvariable(zone_y2_config[CONF_ID], zone_num) | ||||
|             await number.register_number( | ||||
|                 y2, zone_y2_config, min_value=0, max_value=7560, step=1 | ||||
|             ) | ||||
|             await cg.register_parented(y2, config[CONF_LD2450_ID]) | ||||
|  | ||||
|             cg.add(ld2450_component.set_zone_numbers(zone_num, x1, y1, x2, y2)) | ||||
							
								
								
									
										12
									
								
								esphome/components/ld2450/number/presence_timeout_number.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								esphome/components/ld2450/number/presence_timeout_number.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| #include "presence_timeout_number.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| void PresenceTimeoutNumber::control(float value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_presence_timeout(); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										18
									
								
								esphome/components/ld2450/number/presence_timeout_number.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/ld2450/number/presence_timeout_number.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/number/number.h" | ||||
| #include "../ld2450.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| class PresenceTimeoutNumber : public number::Number, public Parented<LD2450Component> { | ||||
|  public: | ||||
|   PresenceTimeoutNumber() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(float value) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										14
									
								
								esphome/components/ld2450/number/zone_coordinate_number.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								esphome/components/ld2450/number/zone_coordinate_number.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| #include "zone_coordinate_number.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| ZoneCoordinateNumber::ZoneCoordinateNumber(uint8_t zone) : zone_(zone) {} | ||||
|  | ||||
| void ZoneCoordinateNumber::control(float value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_zone_coordinate(this->zone_); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										19
									
								
								esphome/components/ld2450/number/zone_coordinate_number.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/ld2450/number/zone_coordinate_number.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/number/number.h" | ||||
| #include "../ld2450.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| class ZoneCoordinateNumber : public number::Number, public Parented<LD2450Component> { | ||||
|  public: | ||||
|   ZoneCoordinateNumber(uint8_t zone); | ||||
|  | ||||
|  protected: | ||||
|   uint8_t zone_; | ||||
|   void control(float value) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										56
									
								
								esphome/components/ld2450/select/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								esphome/components/ld2450/select/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import select | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_BAUD_RATE, ENTITY_CATEGORY_CONFIG, ICON_THERMOMETER | ||||
|  | ||||
| from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns | ||||
|  | ||||
| CONF_ZONE_TYPE = "zone_type" | ||||
|  | ||||
| BaudRateSelect = ld2450_ns.class_("BaudRateSelect", select.Select) | ||||
| ZoneTypeSelect = ld2450_ns.class_("ZoneTypeSelect", select.Select) | ||||
|  | ||||
| CONFIG_SCHEMA = { | ||||
|     cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), | ||||
|     cv.Optional(CONF_BAUD_RATE): select.select_schema( | ||||
|         BaudRateSelect, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         icon=ICON_THERMOMETER, | ||||
|     ), | ||||
|     cv.Optional(CONF_ZONE_TYPE): select.select_schema( | ||||
|         ZoneTypeSelect, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         icon=ICON_THERMOMETER, | ||||
|     ), | ||||
| } | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) | ||||
|     if baud_rate_config := config.get(CONF_BAUD_RATE): | ||||
|         s = await select.new_select( | ||||
|             baud_rate_config, | ||||
|             options=[ | ||||
|                 "9600", | ||||
|                 "19200", | ||||
|                 "38400", | ||||
|                 "57600", | ||||
|                 "115200", | ||||
|                 "230400", | ||||
|                 "256000", | ||||
|                 "460800", | ||||
|             ], | ||||
|         ) | ||||
|         await cg.register_parented(s, config[CONF_LD2450_ID]) | ||||
|         cg.add(ld2450_component.set_baud_rate_select(s)) | ||||
|     if zone_type_config := config.get(CONF_ZONE_TYPE): | ||||
|         s = await select.new_select( | ||||
|             zone_type_config, | ||||
|             options=[ | ||||
|                 "Disabled", | ||||
|                 "Detection", | ||||
|                 "Filter", | ||||
|             ], | ||||
|         ) | ||||
|         await cg.register_parented(s, config[CONF_LD2450_ID]) | ||||
|         cg.add(ld2450_component.set_zone_type_select(s)) | ||||
							
								
								
									
										12
									
								
								esphome/components/ld2450/select/baud_rate_select.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								esphome/components/ld2450/select/baud_rate_select.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| #include "baud_rate_select.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| void BaudRateSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_baud_rate(state); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										18
									
								
								esphome/components/ld2450/select/baud_rate_select.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/ld2450/select/baud_rate_select.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/select/select.h" | ||||
| #include "../ld2450.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| class BaudRateSelect : public select::Select, public Parented<LD2450Component> { | ||||
|  public: | ||||
|   BaudRateSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										12
									
								
								esphome/components/ld2450/select/zone_type_select.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								esphome/components/ld2450/select/zone_type_select.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| #include "zone_type_select.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| void ZoneTypeSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_zone_type(state); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										18
									
								
								esphome/components/ld2450/select/zone_type_select.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/ld2450/select/zone_type_select.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/select/select.h" | ||||
| #include "../ld2450.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| class ZoneTypeSelect : public select::Select, public Parented<LD2450Component> { | ||||
|  public: | ||||
|   ZoneTypeSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										156
									
								
								esphome/components/ld2450/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								esphome/components/ld2450/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ANGLE, | ||||
|     CONF_DISTANCE, | ||||
|     CONF_RESOLUTION, | ||||
|     CONF_SPEED, | ||||
|     DEVICE_CLASS_DISTANCE, | ||||
|     DEVICE_CLASS_SPEED, | ||||
|     UNIT_DEGREES, | ||||
|     UNIT_MILLIMETER, | ||||
| ) | ||||
|  | ||||
| from . import CONF_LD2450_ID, LD2450Component | ||||
|  | ||||
| DEPENDENCIES = ["ld2450"] | ||||
|  | ||||
| CONF_MOVING_TARGET_COUNT = "moving_target_count" | ||||
| CONF_STILL_TARGET_COUNT = "still_target_count" | ||||
| CONF_TARGET_COUNT = "target_count" | ||||
| CONF_X = "x" | ||||
| CONF_Y = "y" | ||||
|  | ||||
| ICON_ACCOUNT_GROUP = "mdi:account-group" | ||||
| ICON_ACCOUNT_SWITCH = "mdi:account-switch" | ||||
| ICON_ALPHA_X_BOX_OUTLINE = "mdi:alpha-x-box-outline" | ||||
| ICON_ALPHA_Y_BOX_OUTLINE = "mdi:alpha-y-box-outline" | ||||
| ICON_FORMAT_TEXT_ROTATION_ANGLE_UP = "mdi:format-text-rotation-angle-up" | ||||
| ICON_HUMAN_GREETING_PROXIMITY = "mdi:human-greeting-proximity" | ||||
| ICON_MAP_MARKER_ACCOUNT = "mdi:map-marker-account" | ||||
| ICON_MAP_MARKER_DISTANCE = "mdi:map-marker-distance" | ||||
| ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE = "mdi:relation-zero-or-one-to-zero-or-one" | ||||
| ICON_SPEEDOMETER_SLOW = "mdi:speedometer-slow" | ||||
|  | ||||
| MAX_TARGETS = 3 | ||||
| MAX_ZONES = 3 | ||||
|  | ||||
| UNIT_MILLIMETER_PER_SECOND = "mm/s" | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), | ||||
|         cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( | ||||
|             icon=ICON_ACCOUNT_GROUP, | ||||
|         ), | ||||
|         cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( | ||||
|             icon=ICON_HUMAN_GREETING_PROXIMITY, | ||||
|         ), | ||||
|         cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( | ||||
|             icon=ICON_ACCOUNT_SWITCH, | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = CONFIG_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(f"target_{n + 1}"): cv.Schema( | ||||
|             { | ||||
|                 cv.Optional(CONF_X): sensor.sensor_schema( | ||||
|                     device_class=DEVICE_CLASS_DISTANCE, | ||||
|                     unit_of_measurement=UNIT_MILLIMETER, | ||||
|                     icon=ICON_ALPHA_X_BOX_OUTLINE, | ||||
|                 ), | ||||
|                 cv.Optional(CONF_Y): sensor.sensor_schema( | ||||
|                     device_class=DEVICE_CLASS_DISTANCE, | ||||
|                     unit_of_measurement=UNIT_MILLIMETER, | ||||
|                     icon=ICON_ALPHA_Y_BOX_OUTLINE, | ||||
|                 ), | ||||
|                 cv.Optional(CONF_SPEED): sensor.sensor_schema( | ||||
|                     device_class=DEVICE_CLASS_SPEED, | ||||
|                     unit_of_measurement=UNIT_MILLIMETER_PER_SECOND, | ||||
|                     icon=ICON_SPEEDOMETER_SLOW, | ||||
|                 ), | ||||
|                 cv.Optional(CONF_ANGLE): sensor.sensor_schema( | ||||
|                     unit_of_measurement=UNIT_DEGREES, | ||||
|                     icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP, | ||||
|                 ), | ||||
|                 cv.Optional(CONF_DISTANCE): sensor.sensor_schema( | ||||
|                     device_class=DEVICE_CLASS_DISTANCE, | ||||
|                     unit_of_measurement=UNIT_MILLIMETER, | ||||
|                     icon=ICON_MAP_MARKER_DISTANCE, | ||||
|                 ), | ||||
|                 cv.Optional(CONF_RESOLUTION): sensor.sensor_schema( | ||||
|                     device_class=DEVICE_CLASS_DISTANCE, | ||||
|                     unit_of_measurement=UNIT_MILLIMETER, | ||||
|                     icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE, | ||||
|                 ), | ||||
|             } | ||||
|         ) | ||||
|         for n in range(MAX_TARGETS) | ||||
|     }, | ||||
|     { | ||||
|         cv.Optional(f"zone_{n + 1}"): cv.Schema( | ||||
|             { | ||||
|                 cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( | ||||
|                     icon=ICON_MAP_MARKER_ACCOUNT, | ||||
|                 ), | ||||
|                 cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( | ||||
|                     icon=ICON_MAP_MARKER_ACCOUNT, | ||||
|                 ), | ||||
|                 cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( | ||||
|                     icon=ICON_MAP_MARKER_ACCOUNT, | ||||
|                 ), | ||||
|             } | ||||
|         ) | ||||
|         for n in range(MAX_ZONES) | ||||
|     }, | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) | ||||
|  | ||||
|     if target_count_config := config.get(CONF_TARGET_COUNT): | ||||
|         sens = await sensor.new_sensor(target_count_config) | ||||
|         cg.add(ld2450_component.set_target_count_sensor(sens)) | ||||
|  | ||||
|     if still_target_count_config := config.get(CONF_STILL_TARGET_COUNT): | ||||
|         sens = await sensor.new_sensor(still_target_count_config) | ||||
|         cg.add(ld2450_component.set_still_target_count_sensor(sens)) | ||||
|  | ||||
|     if moving_target_count_config := config.get(CONF_MOVING_TARGET_COUNT): | ||||
|         sens = await sensor.new_sensor(moving_target_count_config) | ||||
|         cg.add(ld2450_component.set_moving_target_count_sensor(sens)) | ||||
|     for n in range(MAX_TARGETS): | ||||
|         if target_conf := config.get(f"target_{n + 1}"): | ||||
|             if x_config := target_conf.get(CONF_X): | ||||
|                 sens = await sensor.new_sensor(x_config) | ||||
|                 cg.add(ld2450_component.set_move_x_sensor(n, sens)) | ||||
|             if y_config := target_conf.get(CONF_Y): | ||||
|                 sens = await sensor.new_sensor(y_config) | ||||
|                 cg.add(ld2450_component.set_move_y_sensor(n, sens)) | ||||
|             if speed_config := target_conf.get(CONF_SPEED): | ||||
|                 sens = await sensor.new_sensor(speed_config) | ||||
|                 cg.add(ld2450_component.set_move_speed_sensor(n, sens)) | ||||
|             if angle_config := target_conf.get(CONF_ANGLE): | ||||
|                 sens = await sensor.new_sensor(angle_config) | ||||
|                 cg.add(ld2450_component.set_move_angle_sensor(n, sens)) | ||||
|             if distance_config := target_conf.get(CONF_DISTANCE): | ||||
|                 sens = await sensor.new_sensor(distance_config) | ||||
|                 cg.add(ld2450_component.set_move_distance_sensor(n, sens)) | ||||
|             if resolution_config := target_conf.get(CONF_RESOLUTION): | ||||
|                 sens = await sensor.new_sensor(resolution_config) | ||||
|                 cg.add(ld2450_component.set_move_resolution_sensor(n, sens)) | ||||
|     for n in range(MAX_ZONES): | ||||
|         if zone_config := config.get(f"zone_{n + 1}"): | ||||
|             if target_count_config := zone_config.get(CONF_TARGET_COUNT): | ||||
|                 sens = await sensor.new_sensor(target_count_config) | ||||
|                 cg.add(ld2450_component.set_zone_target_count_sensor(n, sens)) | ||||
|             if still_target_count_config := zone_config.get(CONF_STILL_TARGET_COUNT): | ||||
|                 sens = await sensor.new_sensor(still_target_count_config) | ||||
|                 cg.add(ld2450_component.set_zone_still_target_count_sensor(n, sens)) | ||||
|             if moving_target_count_config := zone_config.get(CONF_MOVING_TARGET_COUNT): | ||||
|                 sens = await sensor.new_sensor(moving_target_count_config) | ||||
|                 cg.add(ld2450_component.set_zone_moving_target_count_sensor(n, sens)) | ||||
							
								
								
									
										45
									
								
								esphome/components/ld2450/switch/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								esphome/components/ld2450/switch/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import switch | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     DEVICE_CLASS_SWITCH, | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
|     ICON_BLUETOOTH, | ||||
|     ICON_PULSE, | ||||
| ) | ||||
|  | ||||
| from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns | ||||
|  | ||||
| BluetoothSwitch = ld2450_ns.class_("BluetoothSwitch", switch.Switch) | ||||
| MultiTargetSwitch = ld2450_ns.class_("MultiTargetSwitch", switch.Switch) | ||||
|  | ||||
| CONF_BLUETOOTH = "bluetooth" | ||||
| CONF_MULTI_TARGET = "multi_target" | ||||
|  | ||||
| CONFIG_SCHEMA = { | ||||
|     cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), | ||||
|     cv.Optional(CONF_BLUETOOTH): switch.switch_schema( | ||||
|         BluetoothSwitch, | ||||
|         device_class=DEVICE_CLASS_SWITCH, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         icon=ICON_BLUETOOTH, | ||||
|     ), | ||||
|     cv.Optional(CONF_MULTI_TARGET): switch.switch_schema( | ||||
|         MultiTargetSwitch, | ||||
|         device_class=DEVICE_CLASS_SWITCH, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         icon=ICON_PULSE, | ||||
|     ), | ||||
| } | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) | ||||
|     if bluetooth_config := config.get(CONF_BLUETOOTH): | ||||
|         s = await switch.new_switch(bluetooth_config) | ||||
|         await cg.register_parented(s, config[CONF_LD2450_ID]) | ||||
|         cg.add(ld2450_component.set_bluetooth_switch(s)) | ||||
|     if multi_target_config := config.get(CONF_MULTI_TARGET): | ||||
|         s = await switch.new_switch(multi_target_config) | ||||
|         await cg.register_parented(s, config[CONF_LD2450_ID]) | ||||
|         cg.add(ld2450_component.set_multi_target_switch(s)) | ||||
							
								
								
									
										12
									
								
								esphome/components/ld2450/switch/bluetooth_switch.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								esphome/components/ld2450/switch/bluetooth_switch.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| #include "bluetooth_switch.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| void BluetoothSwitch::write_state(bool state) { | ||||
|   this->publish_state(state); | ||||
|   this->parent_->set_bluetooth(state); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										18
									
								
								esphome/components/ld2450/switch/bluetooth_switch.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/ld2450/switch/bluetooth_switch.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/switch/switch.h" | ||||
| #include "../ld2450.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| class BluetoothSwitch : public switch_::Switch, public Parented<LD2450Component> { | ||||
|  public: | ||||
|   BluetoothSwitch() = default; | ||||
|  | ||||
|  protected: | ||||
|   void write_state(bool state) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										12
									
								
								esphome/components/ld2450/switch/multi_target_switch.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								esphome/components/ld2450/switch/multi_target_switch.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| #include "multi_target_switch.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| void MultiTargetSwitch::write_state(bool state) { | ||||
|   this->publish_state(state); | ||||
|   this->parent_->set_multi_target(state); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										18
									
								
								esphome/components/ld2450/switch/multi_target_switch.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/ld2450/switch/multi_target_switch.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/switch/switch.h" | ||||
| #include "../ld2450.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| class MultiTargetSwitch : public switch_::Switch, public Parented<LD2450Component> { | ||||
|  public: | ||||
|   MultiTargetSwitch() = default; | ||||
|  | ||||
|  protected: | ||||
|   void write_state(bool state) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										62
									
								
								esphome/components/ld2450/text_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								esphome/components/ld2450/text_sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import text_sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DIRECTION, | ||||
|     CONF_MAC_ADDRESS, | ||||
|     CONF_VERSION, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     ENTITY_CATEGORY_NONE, | ||||
|     ICON_BLUETOOTH, | ||||
|     ICON_CHIP, | ||||
|     ICON_SIGN_DIRECTION, | ||||
| ) | ||||
|  | ||||
| from . import CONF_LD2450_ID, LD2450Component | ||||
|  | ||||
| DEPENDENCIES = ["ld2450"] | ||||
|  | ||||
| MAX_TARGETS = 3 | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), | ||||
|         cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( | ||||
|             entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             icon=ICON_CHIP, | ||||
|         ), | ||||
|         cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( | ||||
|             entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             icon=ICON_BLUETOOTH, | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = CONFIG_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(f"target_{n + 1}"): cv.Schema( | ||||
|             { | ||||
|                 cv.Optional(CONF_DIRECTION): text_sensor.text_sensor_schema( | ||||
|                     entity_category=ENTITY_CATEGORY_NONE, | ||||
|                     icon=ICON_SIGN_DIRECTION, | ||||
|                 ), | ||||
|             } | ||||
|         ) | ||||
|         for n in range(MAX_TARGETS) | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) | ||||
|     if version_config := config.get(CONF_VERSION): | ||||
|         sens = await text_sensor.new_text_sensor(version_config) | ||||
|         cg.add(ld2450_component.set_version_text_sensor(sens)) | ||||
|     if mac_address_config := config.get(CONF_MAC_ADDRESS): | ||||
|         sens = await text_sensor.new_text_sensor(mac_address_config) | ||||
|         cg.add(ld2450_component.set_mac_text_sensor(sens)) | ||||
|     for n in range(MAX_TARGETS): | ||||
|         if direction_conf := config.get(f"target_{n + 1}"): | ||||
|             if direction_config := direction_conf.get(CONF_DIRECTION): | ||||
|                 sens = await text_sensor.new_text_sensor(direction_config) | ||||
|                 cg.add(ld2450_component.set_direction_text_sensor(n, sens)) | ||||
| @@ -462,8 +462,6 @@ CONF_LVGL_ID = "lvgl_id" | ||||
| CONF_LONG_MODE = "long_mode" | ||||
| CONF_MSGBOXES = "msgboxes" | ||||
| CONF_OBJ = "obj" | ||||
| CONF_OFFSET_X = "offset_x" | ||||
| CONF_OFFSET_Y = "offset_y" | ||||
| CONF_ONE_CHECKED = "one_checked" | ||||
| CONF_ONE_LINE = "one_line" | ||||
| CONF_ON_PAUSE = "on_pause" | ||||
|   | ||||
| @@ -19,7 +19,7 @@ static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, ui | ||||
|   const auto *gd = fe->get_glyph_data(unicode_letter); | ||||
|   if (gd == nullptr) | ||||
|     return false; | ||||
|   dsc->adv_w = gd->offset_x + gd->width; | ||||
|   dsc->adv_w = gd->advance; | ||||
|   dsc->ofs_x = gd->offset_x; | ||||
|   dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline; | ||||
|   dsc->box_w = gd->width; | ||||
|   | ||||
| @@ -416,37 +416,45 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf | ||||
|       buffer_frac_(buffer_frac), | ||||
|       full_refresh_(full_refresh), | ||||
|       resume_on_input_(resume_on_input) { | ||||
|   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; | ||||
|   this->rotation = display->get_rotation(); | ||||
|   if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { | ||||
|     this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes));  // NOLINT | ||||
|     if (this->rotate_buf_ == nullptr) | ||||
|       return; | ||||
|   } | ||||
|   auto *buf = lv_custom_mem_alloc(buf_bytes);  // NOLINT | ||||
|   if (buf == nullptr) | ||||
|     return; | ||||
|   lv_disp_draw_buf_init(&this->draw_buf_, buf, nullptr, buffer_pixels); | ||||
|   lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0); | ||||
|   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; | ||||
|   this->disp_drv_.hor_res = (lv_coord_t) display->get_width(); | ||||
|   this->disp_drv_.ver_res = (lv_coord_t) display->get_height(); | ||||
|   this->disp_drv_.hor_res = 0; | ||||
|   this->disp_drv_.ver_res = 0; | ||||
|   this->disp_ = lv_disp_drv_register(&this->disp_drv_); | ||||
| } | ||||
|  | ||||
| void LvglComponent::setup() { | ||||
|   if (this->draw_buf_.buf1 == nullptr) { | ||||
|   ESP_LOGCONFIG(TAG, "LVGL Setup starts"); | ||||
|   auto *display = this->displays_[0]; | ||||
|   auto width = display->get_width(); | ||||
|   auto height = display->get_height(); | ||||
|   size_t buffer_pixels = width * height / this->buffer_frac_; | ||||
|   auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; | ||||
|   auto *buffer = lv_custom_mem_alloc(buf_bytes);  // NOLINT | ||||
|   if (buffer == nullptr) { | ||||
|     this->mark_failed(); | ||||
|     this->status_set_error("Memory allocation failure"); | ||||
|     return; | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "LVGL Setup starts"); | ||||
|   lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buf_bytes); | ||||
|   this->disp_drv_.hor_res = width; | ||||
|   this->disp_drv_.ver_res = height; | ||||
|   // this->setup_driver_(display->get_width(), display->get_height()); | ||||
|   lv_disp_drv_update(this->disp_, &this->disp_drv_); | ||||
|   this->rotation = display->get_rotation(); | ||||
|   if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { | ||||
|     this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(this->draw_buf_.size));  // NOLINT | ||||
|     if (this->rotate_buf_ == nullptr) { | ||||
|       this->mark_failed(); | ||||
|       this->status_set_error("Memory allocation failure"); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| #if LV_USE_LOG | ||||
|   lv_log_register_print_cb([](const char *buf) { | ||||
|     auto next = strchr(buf, ')'); | ||||
| @@ -458,8 +466,8 @@ void LvglComponent::setup() { | ||||
|   }); | ||||
| #endif | ||||
|   // Rotation will be handled by our drawing function, so reset the display rotation. | ||||
|   for (auto *display : this->displays_) | ||||
|     display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); | ||||
|   for (auto *disp : this->displays_) | ||||
|     disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); | ||||
|   this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); | ||||
|   lv_disp_trig_activity(this->disp_); | ||||
|   ESP_LOGCONFIG(TAG, "LVGL Setup complete"); | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ANGLE, CONF_MODE | ||||
| from esphome.const import CONF_ANGLE, CONF_MODE, CONF_OFFSET_X, CONF_OFFSET_Y | ||||
|  | ||||
| from ..defines import ( | ||||
|     CONF_ANTIALIAS, | ||||
|     CONF_MAIN, | ||||
|     CONF_OFFSET_X, | ||||
|     CONF_OFFSET_Y, | ||||
|     CONF_PIVOT_X, | ||||
|     CONF_PIVOT_Y, | ||||
|     CONF_SRC, | ||||
|   | ||||
| @@ -550,6 +550,7 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo | ||||
|           cfg3 = MCP_16MHZ_40KBPS_CFG3; | ||||
|           break; | ||||
|         case (canbus::CAN_50KBPS):  //  50Kbps | ||||
|           cfg1 = MCP_16MHZ_50KBPS_CFG1; | ||||
|           cfg2 = MCP_16MHZ_50KBPS_CFG2; | ||||
|           cfg3 = MCP_16MHZ_50KBPS_CFG3; | ||||
|           break; | ||||
|   | ||||
| @@ -1,20 +1,21 @@ | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c, sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_FILTER, | ||||
|     CONF_GAIN, | ||||
|     CONF_ID, | ||||
|     UNIT_MICROTESLA, | ||||
|     UNIT_CELSIUS, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     CONF_OVERSAMPLING, | ||||
|     CONF_RESOLUTION, | ||||
|     CONF_TEMPERATURE, | ||||
|     CONF_TEMPERATURE_COMPENSATION, | ||||
|     ICON_MAGNET, | ||||
|     ICON_THERMOMETER, | ||||
|     CONF_GAIN, | ||||
|     CONF_RESOLUTION, | ||||
|     CONF_OVERSAMPLING, | ||||
|     CONF_FILTER, | ||||
|     CONF_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_MICROTESLA, | ||||
| ) | ||||
| from esphome import pins | ||||
|  | ||||
| CODEOWNERS = ["@functionpointer"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
| @@ -26,30 +27,46 @@ MLX90393Component = mlx90393_ns.class_( | ||||
| ) | ||||
|  | ||||
| GAIN = { | ||||
|     "1X": 7, | ||||
|     "1_33X": 6, | ||||
|     "1_67X": 5, | ||||
|     "2X": 4, | ||||
|     "2_5X": 3, | ||||
|     "3X": 2, | ||||
|     "4X": 1, | ||||
|     "5X": 0, | ||||
|     "1X": 0, | ||||
|     "1_25X": 1, | ||||
|     "1_67X": 2, | ||||
|     "2X": 3, | ||||
|     "2_5X": 4, | ||||
|     "3X": 5, | ||||
|     "3_75X": 6, | ||||
|     "5X": 7, | ||||
| } | ||||
|  | ||||
| RESOLUTION = { | ||||
|     "16BIT": 0, | ||||
|     "17BIT": 1, | ||||
|     "18BIT": 2, | ||||
|     "19BIT": 3, | ||||
|     "DIV_8": 3, | ||||
|     "DIV_4": 2, | ||||
|     "DIV_2": 1, | ||||
|     "DIV_1": 0, | ||||
| } | ||||
|  | ||||
| CONF_X_AXIS = "x_axis" | ||||
| CONF_Y_AXIS = "y_axis" | ||||
| CONF_Z_AXIS = "z_axis" | ||||
| CONF_DRDY_PIN = "drdy_pin" | ||||
| CONF_HALLCONF = "hallconf" | ||||
|  | ||||
|  | ||||
| def mlx90393_axis_schema(default_resolution: str): | ||||
| def _validate(config): | ||||
|     if config[CONF_TEMPERATURE_COMPENSATION]: | ||||
|         for axis in [CONF_X_AXIS, CONF_Y_AXIS, CONF_Z_AXIS]: | ||||
|             if axis not in config: | ||||
|                 continue | ||||
|             if (res := config[axis][CONF_RESOLUTION]) in [ | ||||
|                 "DIV_8", | ||||
|                 "DIV_4", | ||||
|             ]: | ||||
|                 raise cv.Invalid( | ||||
|                     f"{axis}: {CONF_RESOLUTION} cannot be {res} with {CONF_TEMPERATURE_COMPENSATION} enabled" | ||||
|                 ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def mlx90393_axis_schema(): | ||||
|     return sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_MICROTESLA, | ||||
|         accuracy_decimals=0, | ||||
| @@ -58,7 +75,7 @@ def mlx90393_axis_schema(default_resolution: str): | ||||
|     ).extend( | ||||
|         cv.Schema( | ||||
|             { | ||||
|                 cv.Optional(CONF_RESOLUTION, default=default_resolution): cv.enum( | ||||
|                 cv.Optional(CONF_RESOLUTION, default="DIV_4"): cv.enum( | ||||
|                     RESOLUTION, upper=True, space="_" | ||||
|                 ) | ||||
|             } | ||||
| @@ -66,19 +83,19 @@ def mlx90393_axis_schema(default_resolution: str): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(MLX90393Component), | ||||
|             cv.Optional(CONF_GAIN, default="2_5X"): cv.enum( | ||||
|                 GAIN, upper=True, space="_" | ||||
|             ), | ||||
|             cv.Optional(CONF_GAIN, default="1X"): cv.enum(GAIN, upper=True, space="_"), | ||||
|             cv.Optional(CONF_DRDY_PIN): pins.gpio_input_pin_schema, | ||||
|             cv.Optional(CONF_OVERSAMPLING, default=2): cv.int_range(min=0, max=3), | ||||
|             cv.Optional(CONF_OVERSAMPLING, default=0): cv.int_range(min=0, max=3), | ||||
|             cv.Optional(CONF_FILTER, default=6): cv.int_range(min=0, max=7), | ||||
|             cv.Optional(CONF_X_AXIS): mlx90393_axis_schema("19BIT"), | ||||
|             cv.Optional(CONF_Y_AXIS): mlx90393_axis_schema("19BIT"), | ||||
|             cv.Optional(CONF_Z_AXIS): mlx90393_axis_schema("16BIT"), | ||||
|             cv.Optional(CONF_X_AXIS): mlx90393_axis_schema(), | ||||
|             cv.Optional(CONF_Y_AXIS): mlx90393_axis_schema(), | ||||
|             cv.Optional(CONF_Z_AXIS): mlx90393_axis_schema(), | ||||
|             cv.Optional(CONF_TEMPERATURE_COMPENSATION, default=False): bool, | ||||
|             cv.Optional(CONF_HALLCONF, default=0xC): cv.one_of(0xC, 0x0), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=1, | ||||
| @@ -96,7 +113,8 @@ CONFIG_SCHEMA = ( | ||||
|         }, | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x0C)) | ||||
|     .extend(i2c.i2c_device_schema(0x0C)), | ||||
|     _validate, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -111,6 +129,8 @@ async def to_code(config): | ||||
|     cg.add(var.set_gain(GAIN[config[CONF_GAIN]])) | ||||
|     cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) | ||||
|     cg.add(var.set_filter(config[CONF_FILTER])) | ||||
|     cg.add(var.set_temperature_compensation(config[CONF_TEMPERATURE_COMPENSATION])) | ||||
|     cg.add(var.set_hallconf(config[CONF_HALLCONF])) | ||||
|  | ||||
|     if CONF_X_AXIS in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_X_AXIS]) | ||||
|   | ||||
| @@ -43,6 +43,10 @@ void MLX90393Cls::setup() { | ||||
|   this->mlx_.setDigitalFiltering(this->filter_); | ||||
|  | ||||
|   this->mlx_.setTemperatureOverSampling(this->temperature_oversampling_); | ||||
|  | ||||
|   this->mlx_.setTemperatureCompensation(this->temperature_compensation_); | ||||
|  | ||||
|   this->mlx_.setHallConf(this->hallconf_); | ||||
| } | ||||
|  | ||||
| void MLX90393Cls::dump_config() { | ||||
|   | ||||
| @@ -29,7 +29,10 @@ class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90 | ||||
|   void set_resolution(uint8_t xyz, uint8_t res) { resolutions_[xyz] = res; } | ||||
|   void set_filter(uint8_t filter) { filter_ = filter; } | ||||
|   void set_gain(uint8_t gain_sel) { gain_ = gain_sel; } | ||||
|  | ||||
|   void set_temperature_compensation(bool temperature_compensation) { | ||||
|     temperature_compensation_ = temperature_compensation; | ||||
|   } | ||||
|   void set_hallconf(uint8_t hallconf) { hallconf_ = hallconf; } | ||||
|   // overrides for MLX library | ||||
|  | ||||
|   // disable lint because it keeps suggesting const uint8_t *response. | ||||
| @@ -49,9 +52,11 @@ class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90 | ||||
|   sensor::Sensor *t_sensor_{nullptr}; | ||||
|   uint8_t gain_; | ||||
|   uint8_t oversampling_; | ||||
|   uint8_t temperature_oversampling_ = 0; | ||||
|   uint8_t temperature_oversampling_{0}; | ||||
|   uint8_t filter_; | ||||
|   uint8_t resolutions_[3] = {0}; | ||||
|   uint8_t resolutions_[3]{0}; | ||||
|   bool temperature_compensation_{false}; | ||||
|   uint8_t hallconf_{0xC}; | ||||
|   GPIOPin *drdy_pin_{nullptr}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -36,6 +36,7 @@ from esphome.const import ( | ||||
|     CONF_PAYLOAD_AVAILABLE, | ||||
|     CONF_PAYLOAD_NOT_AVAILABLE, | ||||
|     CONF_PORT, | ||||
|     CONF_PUBLISH_NAN_AS_NONE, | ||||
|     CONF_QOS, | ||||
|     CONF_REBOOT_TIMEOUT, | ||||
|     CONF_RETAIN, | ||||
| @@ -49,7 +50,6 @@ from esphome.const import ( | ||||
|     CONF_USE_ABBREVIATIONS, | ||||
|     CONF_USERNAME, | ||||
|     CONF_WILL_MESSAGE, | ||||
|     CONF_PUBLISH_NAN_AS_NONE, | ||||
|     PLATFORM_BK72XX, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
| @@ -406,7 +406,7 @@ async def to_code(config): | ||||
|     if CONF_SSL_FINGERPRINTS in config: | ||||
|         for fingerprint in config[CONF_SSL_FINGERPRINTS]: | ||||
|             arr = [ | ||||
|                 cg.RawExpression(f"0x{fingerprint[i:i + 2]}") for i in range(0, 40, 2) | ||||
|                 cg.RawExpression(f"0x{fingerprint[i : i + 2]}") for i in range(0, 40, 2) | ||||
|             ] | ||||
|             cg.add(var.add_ssl_fingerprint(arr)) | ||||
|         cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1") | ||||
|   | ||||
							
								
								
									
										189
									
								
								esphome/components/msa3xx/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								esphome/components/msa3xx/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_CALIBRATION, | ||||
|     CONF_ID, | ||||
|     CONF_MIRROR_X, | ||||
|     CONF_MIRROR_Y, | ||||
|     CONF_OFFSET_X, | ||||
|     CONF_OFFSET_Y, | ||||
|     CONF_OFFSET_Z, | ||||
|     CONF_RANGE, | ||||
|     CONF_RESOLUTION, | ||||
|     CONF_SWAP_XY, | ||||
|     CONF_TRANSFORM, | ||||
|     CONF_TYPE, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@latonita"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| MULTI_CONF = True | ||||
|  | ||||
| CONF_MSA3XX_ID = "msa3xx_id" | ||||
|  | ||||
| CONF_MIRROR_Z = "mirror_z" | ||||
| CONF_ON_ACTIVE = "on_active" | ||||
| CONF_ON_DOUBLE_TAP = "on_double_tap" | ||||
| CONF_ON_FREEFALL = "on_freefall" | ||||
| CONF_ON_ORIENTATION = "on_orientation" | ||||
| CONF_ON_TAP = "on_tap" | ||||
|  | ||||
| MODEL_MSA301 = "MSA301" | ||||
| MODEL_MSA311 = "MSA311" | ||||
|  | ||||
| msa3xx_ns = cg.esphome_ns.namespace("msa3xx") | ||||
| MSA3xxComponent = msa3xx_ns.class_( | ||||
|     "MSA3xxComponent", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| MSAModels = msa3xx_ns.enum("Model", True) | ||||
| MSA_MODELS = { | ||||
|     MODEL_MSA301: MSAModels.MSA301, | ||||
|     MODEL_MSA311: MSAModels.MSA311, | ||||
| } | ||||
|  | ||||
| MSARange = msa3xx_ns.enum("Range", True) | ||||
| MSA_RANGES = { | ||||
|     "2G": MSARange.RANGE_2G, | ||||
|     "4G": MSARange.RANGE_4G, | ||||
|     "8G": MSARange.RANGE_8G, | ||||
|     "16G": MSARange.RANGE_16G, | ||||
| } | ||||
|  | ||||
| MSAResolution = msa3xx_ns.enum("Resolution", True) | ||||
| RESOLUTIONS_MSA301 = { | ||||
|     14: MSAResolution.RES_14BIT, | ||||
|     12: MSAResolution.RES_12BIT, | ||||
|     10: MSAResolution.RES_10BIT, | ||||
|     8: MSAResolution.RES_8BIT, | ||||
| } | ||||
|  | ||||
| RESOLUTIONS_MSA311 = { | ||||
|     12: MSAResolution.RES_12BIT, | ||||
| } | ||||
|  | ||||
| _COMMON_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(MSA3xxComponent), | ||||
|         cv.Optional(CONF_RANGE, default="2G"): cv.enum(MSA_RANGES, upper=True), | ||||
|         cv.Optional(CONF_CALIBRATION): cv.Schema( | ||||
|             { | ||||
|                 cv.Optional(CONF_OFFSET_X, default=0): cv.float_range( | ||||
|                     min=-4.5, max=4.5 | ||||
|                 ), | ||||
|                 cv.Optional(CONF_OFFSET_Y, default=0): cv.float_range( | ||||
|                     min=-4.5, max=4.5 | ||||
|                 ), | ||||
|                 cv.Optional(CONF_OFFSET_Z, default=0): cv.float_range( | ||||
|                     min=-4.5, max=4.5 | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_TRANSFORM): cv.Schema( | ||||
|             { | ||||
|                 cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, | ||||
|                 cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, | ||||
|                 cv.Optional(CONF_MIRROR_Z, default=False): cv.boolean, | ||||
|                 cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ACTIVE): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_ON_TAP): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_ON_DOUBLE_TAP): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_ON_FREEFALL): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_ON_ORIENTATION): automation.validate_automation(single=True), | ||||
|     } | ||||
| ).extend(cv.polling_component_schema("10s")) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.typed_schema( | ||||
|     { | ||||
|         MODEL_MSA301: _COMMON_SCHEMA.extend( | ||||
|             { | ||||
|                 cv.Optional(CONF_RESOLUTION, default=14): cv.enum(RESOLUTIONS_MSA301), | ||||
|             } | ||||
|         ).extend(i2c.i2c_device_schema(0x26)), | ||||
|         MODEL_MSA311: _COMMON_SCHEMA.extend( | ||||
|             { | ||||
|                 cv.Optional(CONF_RESOLUTION, default=12): cv.enum(RESOLUTIONS_MSA311), | ||||
|             } | ||||
|         ).extend(i2c.i2c_device_schema(0x62)), | ||||
|     }, | ||||
|     upper=True, | ||||
|     enum=MSA_MODELS, | ||||
| ) | ||||
|  | ||||
| MSA_SENSOR_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(CONF_MSA3XX_ID): cv.use_id(MSA3xxComponent), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| 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) | ||||
|  | ||||
|     cg.add(var.set_model(config[CONF_TYPE])) | ||||
|     cg.add(var.set_range(MSA_RANGES[config[CONF_RANGE]])) | ||||
|     cg.add(var.set_resolution(RESOLUTIONS_MSA301[config[CONF_RESOLUTION]])) | ||||
|  | ||||
|     if transform := config.get(CONF_TRANSFORM): | ||||
|         cg.add( | ||||
|             var.set_transform( | ||||
|                 transform[CONF_MIRROR_X], | ||||
|                 transform[CONF_MIRROR_Y], | ||||
|                 transform[CONF_MIRROR_Z], | ||||
|                 transform[CONF_SWAP_XY], | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     if calibration_config := config.get(CONF_CALIBRATION): | ||||
|         cg.add( | ||||
|             var.set_offset( | ||||
|                 calibration_config[CONF_OFFSET_X], | ||||
|                 calibration_config[CONF_OFFSET_Y], | ||||
|                 calibration_config[CONF_OFFSET_Z], | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     # Triggers secton | ||||
|  | ||||
|     if CONF_ON_ORIENTATION in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_orientation_trigger(), | ||||
|             [], | ||||
|             config[CONF_ON_ORIENTATION], | ||||
|         ) | ||||
|  | ||||
|     if CONF_ON_TAP in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_tap_trigger(), | ||||
|             [], | ||||
|             config[CONF_ON_TAP], | ||||
|         ) | ||||
|  | ||||
|     if CONF_ON_DOUBLE_TAP in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_double_tap_trigger(), | ||||
|             [], | ||||
|             config[CONF_ON_DOUBLE_TAP], | ||||
|         ) | ||||
|  | ||||
|     if CONF_ON_ACTIVE in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_active_trigger(), | ||||
|             [], | ||||
|             config[CONF_ON_ACTIVE], | ||||
|         ) | ||||
|  | ||||
|     if CONF_ON_FREEFALL in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_freefall_trigger(), | ||||
|             [], | ||||
|             config[CONF_ON_FREEFALL], | ||||
|         ) | ||||
							
								
								
									
										40
									
								
								esphome/components/msa3xx/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								esphome/components/msa3xx/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import binary_sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ACTIVE, CONF_NAME, DEVICE_CLASS_VIBRATION, ICON_VIBRATE | ||||
|  | ||||
| from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA | ||||
|  | ||||
| CODEOWNERS = ["@latonita"] | ||||
| DEPENDENCIES = ["msa3xx"] | ||||
|  | ||||
| CONF_TAP = "tap" | ||||
| CONF_DOUBLE_TAP = "double_tap" | ||||
|  | ||||
| ICON_TAP = "mdi:gesture-tap" | ||||
| ICON_DOUBLE_TAP = "mdi:gesture-double-tap" | ||||
|  | ||||
| EVENT_SENSORS = (CONF_TAP, CONF_DOUBLE_TAP, CONF_ACTIVE) | ||||
| ICONS = (ICON_TAP, ICON_DOUBLE_TAP, ICON_VIBRATE) | ||||
|  | ||||
| CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(event): cv.maybe_simple_value( | ||||
|             binary_sensor.binary_sensor_schema( | ||||
|                 device_class=DEVICE_CLASS_VIBRATION, | ||||
|                 icon=icon, | ||||
|             ), | ||||
|             key=CONF_NAME, | ||||
|         ) | ||||
|         for event, icon in zip(EVENT_SENSORS, ICONS) | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     hub = await cg.get_variable(config[CONF_MSA3XX_ID]) | ||||
|  | ||||
|     for sensor in EVENT_SENSORS: | ||||
|         if sensor in config: | ||||
|             sens = await binary_sensor.new_binary_sensor(config[sensor]) | ||||
|             cg.add(getattr(hub, f"set_{sensor}_binary_sensor")(sens)) | ||||
							
								
								
									
										417
									
								
								esphome/components/msa3xx/msa3xx.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								esphome/components/msa3xx/msa3xx.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,417 @@ | ||||
| #include "msa3xx.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace msa3xx { | ||||
|  | ||||
| static const char *const TAG = "msa3xx"; | ||||
|  | ||||
| const uint8_t MSA_3XX_PART_ID = 0x13; | ||||
|  | ||||
| const float GRAVITY_EARTH = 9.80665f; | ||||
| const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9);  // LSB to 1 LSB = 3.9mg = 0.0039g | ||||
| const float G_OFFSET_MIN = -4.5f;  // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe | ||||
| const float G_OFFSET_MAX = 4.5f;   // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe | ||||
|  | ||||
| const uint8_t RESOLUTION[] = {14, 12, 10, 8}; | ||||
|  | ||||
| const uint32_t TAP_COOLDOWN_MS = 500; | ||||
| const uint32_t DOUBLE_TAP_COOLDOWN_MS = 500; | ||||
| const uint32_t ACTIVITY_COOLDOWN_MS = 500; | ||||
|  | ||||
| const char *model_to_string(Model model) { | ||||
|   switch (model) { | ||||
|     case Model::MSA301: | ||||
|       return "MSA301"; | ||||
|     case Model::MSA311: | ||||
|       return "MSA311"; | ||||
|     default: | ||||
|       return "Unknown"; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const char *power_mode_to_string(PowerMode power_mode) { | ||||
|   switch (power_mode) { | ||||
|     case PowerMode::NORMAL: | ||||
|       return "Normal"; | ||||
|     case PowerMode::LOW_POWER: | ||||
|       return "Low Power"; | ||||
|     case PowerMode::SUSPEND: | ||||
|       return "Suspend"; | ||||
|     default: | ||||
|       return "Unknown"; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const char *res_to_string(Resolution resolution) { | ||||
|   switch (resolution) { | ||||
|     case Resolution::RES_14BIT: | ||||
|       return "14-bit"; | ||||
|     case Resolution::RES_12BIT: | ||||
|       return "12-bit"; | ||||
|     case Resolution::RES_10BIT: | ||||
|       return "10-bit"; | ||||
|     case Resolution::RES_8BIT: | ||||
|       return "8-bit"; | ||||
|     default: | ||||
|       return "Unknown"; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const char *range_to_string(Range range) { | ||||
|   switch (range) { | ||||
|     case Range::RANGE_2G: | ||||
|       return "±2g"; | ||||
|     case Range::RANGE_4G: | ||||
|       return "±4g"; | ||||
|     case Range::RANGE_8G: | ||||
|       return "±8g"; | ||||
|     case Range::RANGE_16G: | ||||
|       return "±16g"; | ||||
|     default: | ||||
|       return "Unknown"; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const char *bandwidth_to_string(Bandwidth bandwidth) { | ||||
|   switch (bandwidth) { | ||||
|     case Bandwidth::BW_1_95HZ: | ||||
|       return "1.95 Hz"; | ||||
|     case Bandwidth::BW_3_9HZ: | ||||
|       return "3.9 Hz"; | ||||
|     case Bandwidth::BW_7_81HZ: | ||||
|       return "7.81 Hz"; | ||||
|     case Bandwidth::BW_15_63HZ: | ||||
|       return "15.63 Hz"; | ||||
|     case Bandwidth::BW_31_25HZ: | ||||
|       return "31.25 Hz"; | ||||
|     case Bandwidth::BW_62_5HZ: | ||||
|       return "62.5 Hz"; | ||||
|     case Bandwidth::BW_125HZ: | ||||
|       return "125 Hz"; | ||||
|     case Bandwidth::BW_250HZ: | ||||
|       return "250 Hz"; | ||||
|     case Bandwidth::BW_500HZ: | ||||
|       return "500 Hz"; | ||||
|     default: | ||||
|       return "Unknown"; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const char *orientation_xy_to_string(OrientationXY orientation) { | ||||
|   switch (orientation) { | ||||
|     case OrientationXY::PORTRAIT_UPRIGHT: | ||||
|       return "Portrait Upright"; | ||||
|     case OrientationXY::PORTRAIT_UPSIDE_DOWN: | ||||
|       return "Portrait Upside Down"; | ||||
|     case OrientationXY::LANDSCAPE_LEFT: | ||||
|       return "Landscape Left"; | ||||
|     case OrientationXY::LANDSCAPE_RIGHT: | ||||
|       return "Landscape Right"; | ||||
|     default: | ||||
|       return "Unknown"; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const char *orientation_z_to_string(bool orientation) { return orientation ? "Downwards looking" : "Upwards looking"; } | ||||
|  | ||||
| void MSA3xxComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up MSA3xx..."); | ||||
|  | ||||
|   uint8_t part_id{0xff}; | ||||
|   if (!this->read_byte(static_cast<uint8_t>(RegisterMap::PART_ID), &part_id) || (part_id != MSA_3XX_PART_ID)) { | ||||
|     ESP_LOGE(TAG, "Part ID is wrong or missing. Got 0x%02X", part_id); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Resolution LSB/g | ||||
|   // Range : MSA301      : MSA311 | ||||
|   // S2g   : 1024 (2^10) : 4096 (2^12) | ||||
|   // S4g   : 512  (2^9)  : 2048 (2^11) | ||||
|   // S8g   : 256  (2^8)  : 1024 (2^10) | ||||
|   // S16g  : 128  (2^7)  : 512  (2^9) | ||||
|   if (this->model_ == Model::MSA301) { | ||||
|     this->device_params_.accel_data_width = 14; | ||||
|     this->device_params_.scale_factor_exp = static_cast<uint8_t>(this->range_) - 12; | ||||
|   } else if (this->model_ == Model::MSA311) { | ||||
|     this->device_params_.accel_data_width = 12; | ||||
|     this->device_params_.scale_factor_exp = static_cast<uint8_t>(this->range_) - 10; | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "Unknown model"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->setup_odr_(this->data_rate_); | ||||
|   this->setup_power_mode_bandwidth_(this->power_mode_, this->bandwidth_); | ||||
|   this->setup_range_resolution_(this->range_, this->resolution_);                       // 2g...16g, 14...8 bit | ||||
|   this->setup_offset_(this->offset_x_, this->offset_y_, this->offset_z_);               // calibration offsets | ||||
|   this->write_byte(static_cast<uint8_t>(RegisterMap::TAP_DURATION), 0b11000100);        // set tap duration 250ms | ||||
|   this->write_byte(static_cast<uint8_t>(RegisterMap::SWAP_POLARITY), this->swap_.raw);  // set axes polarity | ||||
|   this->write_byte(static_cast<uint8_t>(RegisterMap::INT_SET_0), 0b01110111);           // enable all interrupts | ||||
|   this->write_byte(static_cast<uint8_t>(RegisterMap::INT_SET_1), 0b00011000);           // including orientation | ||||
| } | ||||
|  | ||||
| void MSA3xxComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "MSA3xx:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "Communication with MSA3xx failed!"); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  Model: %s", model_to_string(this->model_)); | ||||
|   ESP_LOGCONFIG(TAG, "  Power Mode: %s", power_mode_to_string(this->power_mode_)); | ||||
|   ESP_LOGCONFIG(TAG, "  Bandwidth: %s", bandwidth_to_string(this->bandwidth_)); | ||||
|   ESP_LOGCONFIG(TAG, "  Range: %s", range_to_string(this->range_)); | ||||
|   ESP_LOGCONFIG(TAG, "  Resolution: %s", res_to_string(this->resolution_)); | ||||
|   ESP_LOGCONFIG(TAG, "  Offsets: {%.3f m/s², %.3f m/s², %.3f m/s²}", this->offset_x_, this->offset_y_, this->offset_z_); | ||||
|   ESP_LOGCONFIG(TAG, "  Transform: {mirror_x=%s, mirror_y=%s, mirror_z=%s, swap_xy=%s}", YESNO(this->swap_.x_polarity), | ||||
|                 YESNO(this->swap_.y_polarity), YESNO(this->swap_.z_polarity), YESNO(this->swap_.x_y_swap)); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   LOG_BINARY_SENSOR("  ", "Tap", this->tap_binary_sensor_); | ||||
|   LOG_BINARY_SENSOR("  ", "Double Tap", this->double_tap_binary_sensor_); | ||||
|   LOG_BINARY_SENSOR("  ", "Active", this->active_binary_sensor_); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
|   LOG_SENSOR("  ", "Acceleration X", this->acceleration_x_sensor_); | ||||
|   LOG_SENSOR("  ", "Acceleration Y", this->acceleration_y_sensor_); | ||||
|   LOG_SENSOR("  ", "Acceleration Z", this->acceleration_z_sensor_); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   LOG_TEXT_SENSOR("  ", "Orientation XY", this->orientation_xy_text_sensor_); | ||||
|   LOG_TEXT_SENSOR("  ", "Orientation Z", this->orientation_z_text_sensor_); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| bool MSA3xxComponent::read_data_() { | ||||
|   uint8_t accel_data[6]; | ||||
|   if (!this->read_bytes(static_cast<uint8_t>(RegisterMap::ACC_X_LSB), accel_data, 6)) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   auto raw_to_x_bit = [](uint16_t lsb, uint16_t msb, uint8_t data_bits) -> uint16_t { | ||||
|     return ((msb << 8) | lsb) >> (16 - data_bits); | ||||
|   }; | ||||
|  | ||||
|   auto lpf = [](float new_value, float old_value, float alpha = 0.5f) { | ||||
|     return alpha * new_value + (1.0f - alpha) * old_value; | ||||
|   }; | ||||
|  | ||||
|   this->data_.lsb_x = | ||||
|       this->twos_complement_(raw_to_x_bit(accel_data[0], accel_data[1], this->device_params_.accel_data_width), | ||||
|                              this->device_params_.accel_data_width); | ||||
|   this->data_.lsb_y = | ||||
|       this->twos_complement_(raw_to_x_bit(accel_data[2], accel_data[3], this->device_params_.accel_data_width), | ||||
|                              this->device_params_.accel_data_width); | ||||
|   this->data_.lsb_z = | ||||
|       this->twos_complement_(raw_to_x_bit(accel_data[4], accel_data[5], this->device_params_.accel_data_width), | ||||
|                              this->device_params_.accel_data_width); | ||||
|  | ||||
|   this->data_.x = lpf(ldexp(this->data_.lsb_x, this->device_params_.scale_factor_exp) * GRAVITY_EARTH, this->data_.x); | ||||
|   this->data_.y = lpf(ldexp(this->data_.lsb_y, this->device_params_.scale_factor_exp) * GRAVITY_EARTH, this->data_.y); | ||||
|   this->data_.z = lpf(ldexp(this->data_.lsb_z, this->device_params_.scale_factor_exp) * GRAVITY_EARTH, this->data_.z); | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| bool MSA3xxComponent::read_motion_status_() { | ||||
|   if (!this->read_byte(static_cast<uint8_t>(RegisterMap::MOTION_INTERRUPT), &this->status_.motion_int.raw)) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   if (!this->read_byte(static_cast<uint8_t>(RegisterMap::ORIENTATION_STATUS), &this->status_.orientation.raw)) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void MSA3xxComponent::loop() { | ||||
|   if (!this->is_ready()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   RegMotionInterrupt old_motion_int = this->status_.motion_int; | ||||
|  | ||||
|   if (!this->read_data_() || !this->read_motion_status_()) { | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->process_motions_(old_motion_int); | ||||
| } | ||||
|  | ||||
| void MSA3xxComponent::update() { | ||||
|   ESP_LOGV(TAG, "Updating MSA3xx..."); | ||||
|  | ||||
|   if (!this->is_ready()) { | ||||
|     ESP_LOGV(TAG, "Component MSA3xx not ready for update"); | ||||
|     return; | ||||
|   } | ||||
|   ESP_LOGV(TAG, "Acceleration: {x = %+1.3f m/s², y = %+1.3f m/s², z = %+1.3f m/s²}; ", this->data_.x, this->data_.y, | ||||
|            this->data_.z); | ||||
|  | ||||
|   ESP_LOGV(TAG, "Orientation: {XY = %s, Z = %s}", orientation_xy_to_string(this->status_.orientation.orient_xy), | ||||
|            orientation_z_to_string(this->status_.orientation.orient_z)); | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
|   if (this->acceleration_x_sensor_ != nullptr) | ||||
|     this->acceleration_x_sensor_->publish_state(this->data_.x); | ||||
|   if (this->acceleration_y_sensor_ != nullptr) | ||||
|     this->acceleration_y_sensor_->publish_state(this->data_.y); | ||||
|   if (this->acceleration_z_sensor_ != nullptr) | ||||
|     this->acceleration_z_sensor_->publish_state(this->data_.z); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   if (this->orientation_xy_text_sensor_ != nullptr && | ||||
|       (this->status_.orientation.orient_xy != this->status_.orientation_old.orient_xy || | ||||
|        this->status_.never_published)) { | ||||
|     this->orientation_xy_text_sensor_->publish_state(orientation_xy_to_string(this->status_.orientation.orient_xy)); | ||||
|   } | ||||
|   if (this->orientation_z_text_sensor_ != nullptr && | ||||
|       (this->status_.orientation.orient_z != this->status_.orientation_old.orient_z || this->status_.never_published)) { | ||||
|     this->orientation_z_text_sensor_->publish_state(orientation_z_to_string(this->status_.orientation.orient_z)); | ||||
|   } | ||||
|   this->status_.orientation_old = this->status_.orientation; | ||||
| #endif | ||||
|  | ||||
|   this->status_.never_published = false; | ||||
|   this->status_clear_warning(); | ||||
| } | ||||
| float MSA3xxComponent::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| void MSA3xxComponent::set_offset(float offset_x, float offset_y, float offset_z) { | ||||
|   this->offset_x_ = offset_x; | ||||
|   this->offset_y_ = offset_y; | ||||
|   this->offset_z_ = offset_z; | ||||
| } | ||||
|  | ||||
| void MSA3xxComponent::set_transform(bool mirror_x, bool mirror_y, bool mirror_z, bool swap_xy) { | ||||
|   this->swap_.x_polarity = mirror_x; | ||||
|   this->swap_.y_polarity = mirror_y; | ||||
|   this->swap_.z_polarity = mirror_z; | ||||
|   this->swap_.x_y_swap = swap_xy; | ||||
| } | ||||
|  | ||||
| void MSA3xxComponent::setup_odr_(DataRate rate) { | ||||
|   RegOutputDataRate reg_odr; | ||||
|   auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::ODR)); | ||||
|   if (reg.has_value()) { | ||||
|     reg_odr.raw = reg.value(); | ||||
|   } else { | ||||
|     reg_odr.raw = 0x0F;  // defaut from datasheet | ||||
|   } | ||||
|  | ||||
|   reg_odr.x_axis_disable = false; | ||||
|   reg_odr.y_axis_disable = false; | ||||
|   reg_odr.z_axis_disable = false; | ||||
|   reg_odr.odr = rate; | ||||
|  | ||||
|   this->write_byte(static_cast<uint8_t>(RegisterMap::ODR), reg_odr.raw); | ||||
| } | ||||
|  | ||||
| void MSA3xxComponent::setup_power_mode_bandwidth_(PowerMode power_mode, Bandwidth bandwidth) { | ||||
|   // 0x11 POWER_MODE_BANDWIDTH | ||||
|   auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::POWER_MODE_BANDWIDTH)); | ||||
|  | ||||
|   RegPowerModeBandwidth power_mode_bandwidth; | ||||
|   if (reg.has_value()) { | ||||
|     power_mode_bandwidth.raw = reg.value(); | ||||
|   } else { | ||||
|     power_mode_bandwidth.raw = 0xde;  // defaut from datasheet | ||||
|   } | ||||
|  | ||||
|   power_mode_bandwidth.power_mode = power_mode; | ||||
|   power_mode_bandwidth.low_power_bandwidth = bandwidth; | ||||
|  | ||||
|   this->write_byte(static_cast<uint8_t>(RegisterMap::POWER_MODE_BANDWIDTH), power_mode_bandwidth.raw); | ||||
| } | ||||
|  | ||||
| void MSA3xxComponent::setup_range_resolution_(Range range, Resolution resolution) { | ||||
|   RegRangeResolution reg; | ||||
|   reg.raw = this->read_byte(static_cast<uint8_t>(RegisterMap::RANGE_RESOLUTION)).value_or(0x00); | ||||
|   reg.range = range; | ||||
|   if (this->model_ == Model::MSA301) { | ||||
|     reg.resolution = resolution; | ||||
|   } | ||||
|   this->write_byte(static_cast<uint8_t>(RegisterMap::RANGE_RESOLUTION), reg.raw); | ||||
| } | ||||
|  | ||||
| void MSA3xxComponent::setup_offset_(float offset_x, float offset_y, float offset_z) { | ||||
|   uint8_t offset[3]; | ||||
|  | ||||
|   auto offset_g_to_lsb = [](float accel) -> int8_t { | ||||
|     float acccel_clamped = clamp(accel, G_OFFSET_MIN, G_OFFSET_MAX); | ||||
|     return static_cast<int8_t>(acccel_clamped * LSB_COEFF); | ||||
|   }; | ||||
|  | ||||
|   offset[0] = offset_g_to_lsb(offset_x); | ||||
|   offset[1] = offset_g_to_lsb(offset_y); | ||||
|   offset[2] = offset_g_to_lsb(offset_z); | ||||
|  | ||||
|   ESP_LOGV(TAG, "Offset (%.3f, %.3f, %.3f)=>LSB(%d, %d, %d)", offset_x, offset_y, offset_z, offset[0], offset[1], | ||||
|            offset[2]); | ||||
|  | ||||
|   this->write_bytes(static_cast<uint8_t>(RegisterMap::OFFSET_COMP_X), (uint8_t *) &offset, 3); | ||||
| } | ||||
|  | ||||
| int64_t MSA3xxComponent::twos_complement_(uint64_t value, uint8_t bits) { | ||||
|   if (value > (1ULL << (bits - 1))) { | ||||
|     return (int64_t) (value - (1ULL << bits)); | ||||
|   } else { | ||||
|     return (int64_t) value; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void binary_event_debounce(bool state, bool old_state, uint32_t now, uint32_t &last_ms, Trigger<> &trigger, | ||||
|                            uint32_t cooldown_ms, void *bs, const char *desc) { | ||||
|   if (state && now - last_ms > cooldown_ms) { | ||||
|     ESP_LOGV(TAG, "%s detected", desc); | ||||
|     trigger.trigger(); | ||||
|     last_ms = now; | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|     if (bs != nullptr) { | ||||
|       static_cast<binary_sensor::BinarySensor *>(bs)->publish_state(true); | ||||
|     } | ||||
| #endif | ||||
|   } else if (!state && now - last_ms > cooldown_ms && bs != nullptr) { | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|     static_cast<binary_sensor::BinarySensor *>(bs)->publish_state(false); | ||||
| #endif | ||||
|   } | ||||
| } | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| #define BS_OPTIONAL_PTR(x) ((void *) (x)) | ||||
| #else | ||||
| #define BS_OPTIONAL_PTR(x) (nullptr) | ||||
| #endif | ||||
|  | ||||
| void MSA3xxComponent::process_motions_(RegMotionInterrupt old) { | ||||
|   uint32_t now = millis(); | ||||
|  | ||||
|   binary_event_debounce(this->status_.motion_int.single_tap_interrupt, old.single_tap_interrupt, now, | ||||
|                         this->status_.last_tap_ms, this->tap_trigger_, TAP_COOLDOWN_MS, | ||||
|                         BS_OPTIONAL_PTR(this->tap_binary_sensor_), "Tap"); | ||||
|   binary_event_debounce(this->status_.motion_int.double_tap_interrupt, old.double_tap_interrupt, now, | ||||
|                         this->status_.last_double_tap_ms, this->double_tap_trigger_, DOUBLE_TAP_COOLDOWN_MS, | ||||
|                         BS_OPTIONAL_PTR(this->double_tap_binary_sensor_), "Double Tap"); | ||||
|   binary_event_debounce(this->status_.motion_int.active_interrupt, old.active_interrupt, now, | ||||
|                         this->status_.last_action_ms, this->active_trigger_, ACTIVITY_COOLDOWN_MS, | ||||
|                         BS_OPTIONAL_PTR(this->active_binary_sensor_), "Activity"); | ||||
|  | ||||
|   if (this->status_.motion_int.orientation_interrupt) { | ||||
|     ESP_LOGVV(TAG, "Orientation changed"); | ||||
|     this->orientation_trigger_.trigger(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace msa3xx | ||||
| }  // namespace esphome | ||||
							
								
								
									
										311
									
								
								esphome/components/msa3xx/msa3xx.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								esphome/components/msa3xx/msa3xx.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,311 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/core/automation.h" | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
| #endif | ||||
| #ifdef USE_SENSOR | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
| #include "esphome/components/text_sensor/text_sensor.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace msa3xx { | ||||
|  | ||||
| // Combined register map of MSA301 and MSA311 | ||||
| // Differences | ||||
| //  What             |  MSA301 | MSA11  | | ||||
| //  - Resolution     |  14-bit | 12-bit | | ||||
| // | ||||
|  | ||||
| // I2c address | ||||
| enum class Model : uint8_t { | ||||
|   MSA301 = 0x26, | ||||
|   MSA311 = 0x62, | ||||
| }; | ||||
|  | ||||
| // Combined MSA301 and MSA311 register map | ||||
| enum class RegisterMap : uint8_t { | ||||
|   SOFT_RESET = 0x00, | ||||
|   PART_ID = 0x01, | ||||
|   ACC_X_LSB = 0x02, | ||||
|   ACC_X_MSB = 0x03, | ||||
|   ACC_Y_LSB = 0x04, | ||||
|   ACC_Y_MSB = 0x05, | ||||
|   ACC_Z_LSB = 0x06, | ||||
|   ACC_Z_MSB = 0x07, | ||||
|   MOTION_INTERRUPT = 0x09, | ||||
|   DATA_INTERRUPT = 0x0A, | ||||
|   TAP_ACTIVE_STATUS = 0x0B, | ||||
|   ORIENTATION_STATUS = 0x0C, | ||||
|   RESOLUTION_RANGE_CONFIG = 0x0D, | ||||
|   RANGE_RESOLUTION = 0x0F, | ||||
|   ODR = 0x10, | ||||
|   POWER_MODE_BANDWIDTH = 0x11, | ||||
|   SWAP_POLARITY = 0x12, | ||||
|   INT_SET_0 = 0x16, | ||||
|   INT_SET_1 = 0x17, | ||||
|   INT_MAP_0 = 0x19, | ||||
|   INT_MAP_1 = 0x1A, | ||||
|   INT_CONFIG = 0x20, | ||||
|   INT_LATCH = 0x21, | ||||
|   FREEFALL_DURATION = 0x22, | ||||
|   FREEFALL_THRESHOLD = 0x23, | ||||
|   FREEFALL_HYSTERESIS = 0x24, | ||||
|   ACTIVE_DURATION = 0x27, | ||||
|   ACTIVE_THRESHOLD = 0x28, | ||||
|   TAP_DURATION = 0x2A, | ||||
|   TAP_THRESHOLD = 0x2B, | ||||
|   ORIENTATION_CONFIG = 0x2C, | ||||
|   Z_BLOCK = 0x2D, | ||||
|   OFFSET_COMP_X = 0x38, | ||||
|   OFFSET_COMP_Y = 0x39, | ||||
|   OFFSET_COMP_Z = 0x3A, | ||||
| }; | ||||
|  | ||||
| enum class Range : uint8_t { | ||||
|   RANGE_2G = 0b00, | ||||
|   RANGE_4G = 0b01, | ||||
|   RANGE_8G = 0b10, | ||||
|   RANGE_16G = 0b11, | ||||
| }; | ||||
|  | ||||
| enum class Resolution : uint8_t { | ||||
|   RES_14BIT = 0b00, | ||||
|   RES_12BIT = 0b01, | ||||
|   RES_10BIT = 0b10, | ||||
|   RES_8BIT = 0b11, | ||||
| }; | ||||
|  | ||||
| enum class PowerMode : uint8_t { | ||||
|   NORMAL = 0b00, | ||||
|   LOW_POWER = 0b01, | ||||
|   SUSPEND = 0b11, | ||||
| }; | ||||
|  | ||||
| enum class Bandwidth : uint8_t { | ||||
|   BW_1_95HZ = 0b0000, | ||||
|   BW_3_9HZ = 0b0011, | ||||
|   BW_7_81HZ = 0b0100, | ||||
|   BW_15_63HZ = 0b0101, | ||||
|   BW_31_25HZ = 0b0110, | ||||
|   BW_62_5HZ = 0b0111, | ||||
|   BW_125HZ = 0b1000, | ||||
|   BW_250HZ = 0b1001, | ||||
|   BW_500HZ = 0b1010, | ||||
| }; | ||||
|  | ||||
| enum class DataRate : uint8_t { | ||||
|   ODR_1HZ = 0b0000,     // not available in normal mode | ||||
|   ODR_1_95HZ = 0b0001,  // not available in normal mode | ||||
|   ODR_3_9HZ = 0b0010, | ||||
|   ODR_7_81HZ = 0b0011, | ||||
|   ODR_15_63HZ = 0b0100, | ||||
|   ODR_31_25HZ = 0b0101, | ||||
|   ODR_62_5HZ = 0b0110, | ||||
|   ODR_125HZ = 0b0111, | ||||
|   ODR_250HZ = 0b1000, | ||||
|   ODR_500HZ = 0b1001,   // not available in low power mode | ||||
|   ODR_1000HZ = 0b1010,  // not available in low power mode | ||||
| }; | ||||
|  | ||||
| enum class OrientationXY : uint8_t { | ||||
|   PORTRAIT_UPRIGHT = 0b00, | ||||
|   PORTRAIT_UPSIDE_DOWN = 0b01, | ||||
|   LANDSCAPE_LEFT = 0b10, | ||||
|   LANDSCAPE_RIGHT = 0b11, | ||||
| }; | ||||
|  | ||||
| union Orientation { | ||||
|   struct { | ||||
|     OrientationXY xy : 2; | ||||
|     bool z : 1; | ||||
|     uint8_t reserved : 5; | ||||
|   } __attribute__((packed)); | ||||
|   uint8_t raw; | ||||
| }; | ||||
|  | ||||
| // 0x09 | ||||
| union RegMotionInterrupt { | ||||
|   struct { | ||||
|     bool freefall_interrupt : 1; | ||||
|     bool reserved_1 : 1; | ||||
|     bool active_interrupt : 1; | ||||
|     bool reserved_3 : 1; | ||||
|     bool double_tap_interrupt : 1; | ||||
|     bool single_tap_interrupt : 1; | ||||
|     bool orientation_interrupt : 1; | ||||
|     bool reserved_7 : 1; | ||||
|   } __attribute__((packed)); | ||||
|   uint8_t raw; | ||||
| }; | ||||
|  | ||||
| // 0x0C | ||||
| union RegOrientationStatus { | ||||
|   struct { | ||||
|     uint8_t reserved_0_3 : 4; | ||||
|     OrientationXY orient_xy : 2; | ||||
|     bool orient_z : 1; | ||||
|     uint8_t reserved_7 : 1; | ||||
|   } __attribute__((packed)); | ||||
|   uint8_t raw{0x00}; | ||||
| }; | ||||
|  | ||||
| // 0x0f | ||||
| union RegRangeResolution { | ||||
|   struct { | ||||
|     Range range : 2; | ||||
|     Resolution resolution : 2; | ||||
|     uint8_t reserved_2 : 4; | ||||
|   } __attribute__((packed)); | ||||
|   uint8_t raw{0x00}; | ||||
| }; | ||||
|  | ||||
| // 0x10 | ||||
| union RegOutputDataRate { | ||||
|   struct { | ||||
|     DataRate odr : 4; | ||||
|     uint8_t reserved_4 : 1; | ||||
|     bool z_axis_disable : 1; | ||||
|     bool y_axis_disable : 1; | ||||
|     bool x_axis_disable : 1; | ||||
|   } __attribute__((packed)); | ||||
|   uint8_t raw{0xde}; | ||||
| }; | ||||
|  | ||||
| // 0x11 | ||||
| union RegPowerModeBandwidth { | ||||
|   struct { | ||||
|     uint8_t reserved_0 : 1; | ||||
|     Bandwidth low_power_bandwidth : 4; | ||||
|     uint8_t reserved_5 : 1; | ||||
|     PowerMode power_mode : 2; | ||||
|   } __attribute__((packed)); | ||||
|   uint8_t raw{0xde}; | ||||
| }; | ||||
|  | ||||
| // 0x12 | ||||
| union RegSwapPolarity { | ||||
|   struct { | ||||
|     bool x_y_swap : 1; | ||||
|     bool z_polarity : 1; | ||||
|     bool y_polarity : 1; | ||||
|     bool x_polarity : 1; | ||||
|     uint8_t reserved : 4; | ||||
|   } __attribute__((packed)); | ||||
|   uint8_t raw{0}; | ||||
| }; | ||||
|  | ||||
| // 0x2a | ||||
| union RegTapDuration { | ||||
|   struct { | ||||
|     uint8_t duration : 3; | ||||
|     uint8_t reserved : 3; | ||||
|     bool tap_shock : 1; | ||||
|     bool tap_quiet : 1; | ||||
|   } __attribute__((packed)); | ||||
|   uint8_t raw{0x04}; | ||||
| }; | ||||
|  | ||||
| class MSA3xxComponent : public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void loop() override; | ||||
|   void update() override; | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|   void set_model(Model model) { this->model_ = model; } | ||||
|   void set_offset(float offset_x, float offset_y, float offset_z); | ||||
|   void set_range(Range range) { this->range_ = range; } | ||||
|   void set_bandwidth(Bandwidth bandwidth) { this->bandwidth_ = bandwidth; } | ||||
|   void set_resolution(Resolution resolution) { this->resolution_ = resolution; } | ||||
|   void set_transform(bool mirror_x, bool mirror_y, bool mirror_z, bool swap_xy); | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   SUB_BINARY_SENSOR(tap) | ||||
|   SUB_BINARY_SENSOR(double_tap) | ||||
|   SUB_BINARY_SENSOR(active) | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
|   SUB_SENSOR(acceleration_x) | ||||
|   SUB_SENSOR(acceleration_y) | ||||
|   SUB_SENSOR(acceleration_z) | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   SUB_TEXT_SENSOR(orientation_xy) | ||||
|   SUB_TEXT_SENSOR(orientation_z) | ||||
| #endif | ||||
|  | ||||
|   Trigger<> *get_tap_trigger() { return &this->tap_trigger_; } | ||||
|   Trigger<> *get_double_tap_trigger() { return &this->double_tap_trigger_; } | ||||
|   Trigger<> *get_orientation_trigger() { return &this->orientation_trigger_; } | ||||
|   Trigger<> *get_freefall_trigger() { return &this->freefall_trigger_; } | ||||
|   Trigger<> *get_active_trigger() { return &this->active_trigger_; } | ||||
|  | ||||
|  protected: | ||||
|   Model model_{Model::MSA311}; | ||||
|  | ||||
|   PowerMode power_mode_{PowerMode::NORMAL}; | ||||
|   DataRate data_rate_{DataRate::ODR_250HZ}; | ||||
|   Bandwidth bandwidth_{Bandwidth::BW_250HZ}; | ||||
|   Range range_{Range::RANGE_2G}; | ||||
|   Resolution resolution_{Resolution::RES_14BIT}; | ||||
|   float offset_x_, offset_y_, offset_z_;  // in m/s² | ||||
|   RegSwapPolarity swap_; | ||||
|  | ||||
|   struct { | ||||
|     int scale_factor_exp; | ||||
|     uint8_t accel_data_width; | ||||
|   } device_params_{}; | ||||
|  | ||||
|   struct { | ||||
|     int16_t lsb_x, lsb_y, lsb_z; | ||||
|     float x, y, z; | ||||
|   } data_{}; | ||||
|  | ||||
|   struct { | ||||
|     RegMotionInterrupt motion_int; | ||||
|     RegOrientationStatus orientation; | ||||
|     RegOrientationStatus orientation_old; | ||||
|  | ||||
|     uint32_t last_tap_ms{0}; | ||||
|     uint32_t last_double_tap_ms{0}; | ||||
|     uint32_t last_action_ms{0}; | ||||
|  | ||||
|     bool never_published{true}; | ||||
|   } status_{}; | ||||
|  | ||||
|   void setup_odr_(DataRate rate); | ||||
|   void setup_power_mode_bandwidth_(PowerMode power_mode, Bandwidth bandwidth); | ||||
|   void setup_range_resolution_(Range range, Resolution resolution); | ||||
|   void setup_offset_(float offset_x, float offset_y, float offset_z); | ||||
|  | ||||
|   bool read_data_(); | ||||
|   bool read_motion_status_(); | ||||
|  | ||||
|   int64_t twos_complement_(uint64_t value, uint8_t bits); | ||||
|  | ||||
|   // | ||||
|   // Actons / Triggers | ||||
|   // | ||||
|   Trigger<> tap_trigger_; | ||||
|   Trigger<> double_tap_trigger_; | ||||
|   Trigger<> orientation_trigger_; | ||||
|   Trigger<> freefall_trigger_; | ||||
|   Trigger<> active_trigger_; | ||||
|  | ||||
|   void process_motions_(RegMotionInterrupt old); | ||||
| }; | ||||
|  | ||||
| }  // namespace msa3xx | ||||
| }  // namespace esphome | ||||
							
								
								
									
										42
									
								
								esphome/components/msa3xx/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/msa3xx/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ACCELERATION_X, | ||||
|     CONF_ACCELERATION_Y, | ||||
|     CONF_ACCELERATION_Z, | ||||
|     CONF_NAME, | ||||
|     ICON_BRIEFCASE_DOWNLOAD, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_METER_PER_SECOND_SQUARED, | ||||
| ) | ||||
|  | ||||
| from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA | ||||
|  | ||||
| CODEOWNERS = ["@latonita"] | ||||
| DEPENDENCIES = ["msa3xx"] | ||||
|  | ||||
| ACCELERATION_SENSORS = (CONF_ACCELERATION_X, CONF_ACCELERATION_Y, CONF_ACCELERATION_Z) | ||||
|  | ||||
| accel_schema = cv.maybe_simple_value( | ||||
|     sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_METER_PER_SECOND_SQUARED, | ||||
|         icon=ICON_BRIEFCASE_DOWNLOAD, | ||||
|         accuracy_decimals=2, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     key=CONF_NAME, | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend( | ||||
|     {cv.Optional(sensor): accel_schema for sensor in ACCELERATION_SENSORS} | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     hub = await cg.get_variable(config[CONF_MSA3XX_ID]) | ||||
|     for accel_key in ACCELERATION_SENSORS: | ||||
|         if accel_key in config: | ||||
|             sens = await sensor.new_sensor(config[accel_key]) | ||||
|             cg.add(getattr(hub, f"set_{accel_key}_sensor")(sens)) | ||||
							
								
								
									
										38
									
								
								esphome/components/msa3xx/text_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/msa3xx/text_sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import text_sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_NAME | ||||
|  | ||||
| from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA | ||||
|  | ||||
| CODEOWNERS = ["@latonita"] | ||||
| DEPENDENCIES = ["msa3xx"] | ||||
|  | ||||
| CONF_ORIENTATION_XY = "orientation_xy" | ||||
| CONF_ORIENTATION_Z = "orientation_z" | ||||
| ICON_SCREEN_ROTATION = "mdi:screen-rotation" | ||||
|  | ||||
| ORIENTATION_SENSORS = (CONF_ORIENTATION_XY, CONF_ORIENTATION_Z) | ||||
|  | ||||
| CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Optional(sensor): cv.maybe_simple_value( | ||||
|             text_sensor.text_sensor_schema(icon=ICON_SCREEN_ROTATION), | ||||
|             key=CONF_NAME, | ||||
|         ) | ||||
|         for sensor in ORIENTATION_SENSORS | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def setup_conf(config, key, hub): | ||||
|     if sensor_config := config.get(key): | ||||
|         var = await text_sensor.new_text_sensor(sensor_config) | ||||
|         cg.add(getattr(hub, f"set_{key}_text_sensor")(var)) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     hub = await cg.get_variable(config[CONF_MSA3XX_ID]) | ||||
|  | ||||
|     for key in ORIENTATION_SENSORS: | ||||
|         await setup_conf(config, key, hub) | ||||
| @@ -1,9 +1,10 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_UID | ||||
| from esphome.core import HexInt | ||||
| from .. import nfc_ns, Nfcc, NfcTagListener | ||||
|  | ||||
| from .. import Nfcc, NfcTagListener, nfc_ns | ||||
|  | ||||
| DEPENDENCIES = ["nfc"] | ||||
|  | ||||
| @@ -25,8 +26,7 @@ def validate_uid(value): | ||||
|     for x in value.split("-"): | ||||
|         if len(x) != 2: | ||||
|             raise cv.Invalid( | ||||
|                 "Each part (separated by '-') of the UID must be two characters " | ||||
|                 "long." | ||||
|                 "Each part (separated by '-') of the UID must be two characters long." | ||||
|             ) | ||||
|         try: | ||||
|             x = int(x, 16) | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| from typing import Any | ||||
|  | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor | ||||
| from .. import const, schema, validate, generate | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from .. import const, generate, schema, validate | ||||
|  | ||||
| DEPENDENCIES = [const.OPENTHERM] | ||||
| COMPONENT_TYPE = const.BINARY_SENSOR | ||||
| @@ -11,8 +12,7 @@ COMPONENT_TYPE = const.BINARY_SENSOR | ||||
| def get_entity_validation_schema(entity: schema.BinarySensorSchema) -> cv.Schema: | ||||
|     return binary_sensor.binary_sensor_schema( | ||||
|         device_class=( | ||||
|             entity.device_class | ||||
|             or binary_sensor._UNDEF  # pylint: disable=protected-access | ||||
|             entity.device_class or binary_sensor._UNDEF  # pylint: disable=protected-access | ||||
|         ), | ||||
|         icon=(entity.icon or binary_sensor._UNDEF),  # pylint: disable=protected-access | ||||
|     ) | ||||
|   | ||||
| @@ -3,8 +3,9 @@ from typing import Any, Callable, Optional | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
| from . import const | ||||
| from .schema import TSchema, SettingSchema | ||||
| from .schema import SettingSchema, TSchema | ||||
|  | ||||
| opentherm_ns = cg.esphome_ns.namespace("opentherm") | ||||
| OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) | ||||
| @@ -112,8 +113,7 @@ def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): | ||||
|         msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}") | ||||
|         if keep_updated: | ||||
|             cg.add(hub.add_repeating_message(msg_expr)) | ||||
|         else: | ||||
|             if order is not None: | ||||
|         elif order is not None: | ||||
|             cg.add(hub.add_initial_message(msg_expr, order)) | ||||
|         else: | ||||
|             cg.add(hub.add_initial_message(msg_expr)) | ||||
| @@ -128,7 +128,7 @@ Create = Callable[[dict[str, Any], str, cg.MockObj], Awaitable[cg.Pvariable]] | ||||
|  | ||||
|  | ||||
| def create_only_conf( | ||||
|     create: Callable[[dict[str, Any]], Awaitable[cg.Pvariable]] | ||||
|     create: Callable[[dict[str, Any]], Awaitable[cg.Pvariable]], | ||||
| ) -> Create: | ||||
|     return lambda conf, _key, _hub: create(conf) | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| from typing import Any | ||||
|  | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor | ||||
| from .. import const, schema, validate, generate | ||||
| import esphome.config_validation as cv | ||||
|  | ||||
| from .. import const, generate, schema, validate | ||||
|  | ||||
| DEPENDENCIES = [const.OPENTHERM] | ||||
| COMPONENT_TYPE = const.SENSOR | ||||
| @@ -22,11 +23,9 @@ MSG_DATA_TYPES = { | ||||
|  | ||||
| def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema: | ||||
|     return sensor.sensor_schema( | ||||
|         unit_of_measurement=entity.unit_of_measurement | ||||
|         or sensor._UNDEF,  # pylint: disable=protected-access | ||||
|         unit_of_measurement=entity.unit_of_measurement or sensor._UNDEF,  # pylint: disable=protected-access | ||||
|         accuracy_decimals=entity.accuracy_decimals, | ||||
|         device_class=entity.device_class | ||||
|         or sensor._UNDEF,  # pylint: disable=protected-access | ||||
|         device_class=entity.device_class or sensor._UNDEF,  # pylint: disable=protected-access | ||||
|         icon=entity.icon or sensor._UNDEF,  # pylint: disable=protected-access | ||||
|         state_class=entity.state_class, | ||||
|     ).extend( | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| from pathlib import Path | ||||
|  | ||||
| import esphome.config_validation as cv | ||||
| from esphome import git, yaml_util | ||||
| from esphome.config_helpers import merge_config | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ESPHOME, | ||||
|     CONF_FILE, | ||||
| @@ -10,12 +10,14 @@ from esphome.const import ( | ||||
|     CONF_MIN_VERSION, | ||||
|     CONF_PACKAGES, | ||||
|     CONF_PASSWORD, | ||||
|     CONF_PATH, | ||||
|     CONF_REF, | ||||
|     CONF_REFRESH, | ||||
|     CONF_URL, | ||||
|     CONF_USERNAME, | ||||
|     CONF_VARS, | ||||
|     __version__ as ESPHOME_VERSION, | ||||
| ) | ||||
| from esphome.const import __version__ as ESPHOME_VERSION | ||||
| from esphome.core import EsphomeError | ||||
|  | ||||
| DOMAIN = CONF_PACKAGES | ||||
| @@ -74,7 +76,19 @@ BASE_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_PASSWORD): cv.string, | ||||
|             cv.Exclusive(CONF_FILE, "files"): validate_yaml_filename, | ||||
|             cv.Exclusive(CONF_FILES, "files"): cv.All( | ||||
|                 cv.ensure_list(validate_yaml_filename), | ||||
|                 cv.ensure_list( | ||||
|                     cv.Any( | ||||
|                         validate_yaml_filename, | ||||
|                         cv.Schema( | ||||
|                             { | ||||
|                                 cv.Required(CONF_PATH): validate_yaml_filename, | ||||
|                                 cv.Optional(CONF_VARS, default={}): cv.Schema( | ||||
|                                     {cv.string: cv.string} | ||||
|                                 ), | ||||
|                             } | ||||
|                         ), | ||||
|                     ) | ||||
|                 ), | ||||
|                 cv.Length(min=1), | ||||
|             ), | ||||
|             cv.Optional(CONF_REF): cv.git_ref, | ||||
| @@ -106,16 +120,25 @@ def _process_base_package(config: dict) -> dict: | ||||
|         username=config.get(CONF_USERNAME), | ||||
|         password=config.get(CONF_PASSWORD), | ||||
|     ) | ||||
|     files: list[str] = config[CONF_FILES] | ||||
|     files = [] | ||||
|  | ||||
|     for file in config[CONF_FILES]: | ||||
|         if isinstance(file, str): | ||||
|             files.append({CONF_PATH: file, CONF_VARS: {}}) | ||||
|         else: | ||||
|             files.append(file) | ||||
|  | ||||
|     def get_packages(files) -> dict: | ||||
|         packages = {} | ||||
|         for file in files: | ||||
|             yaml_file: Path = repo_dir / file | ||||
|         for idx, file in enumerate(files): | ||||
|             filename = file[CONF_PATH] | ||||
|             yaml_file: Path = repo_dir / filename | ||||
|             vars = file.get(CONF_VARS, {}) | ||||
|  | ||||
|             if not yaml_file.is_file(): | ||||
|                 raise cv.Invalid( | ||||
|                     f"{file} does not exist in repository", path=[CONF_FILES] | ||||
|                     f"{filename} does not exist in repository", | ||||
|                     path=[CONF_FILES, idx, CONF_PATH], | ||||
|                 ) | ||||
|  | ||||
|             try: | ||||
| @@ -131,11 +154,12 @@ def _process_base_package(config: dict) -> dict: | ||||
|                         raise cv.Invalid( | ||||
|                             f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" | ||||
|                         ) | ||||
|  | ||||
|                 packages[file] = new_yaml | ||||
|                 vars = {k: str(v) for k, v in vars.items()} | ||||
|                 new_yaml = yaml_util.substitute_vars(new_yaml, vars) | ||||
|                 packages[f"{filename}{idx}"] = new_yaml | ||||
|             except EsphomeError as e: | ||||
|                 raise cv.Invalid( | ||||
|                     f"{file} is not a valid YAML file. Please check the file contents.\n{e}" | ||||
|                     f"{filename} is not a valid YAML file. Please check the file contents.\n{e}" | ||||
|                 ) from e | ||||
|         return packages | ||||
|  | ||||
| @@ -154,7 +178,7 @@ def _process_base_package(config: dict) -> dict: | ||||
|             error = er | ||||
|  | ||||
|     if packages is None: | ||||
|         raise cv.Invalid(f"Failed to load packages. {error}") | ||||
|         raise cv.Invalid(f"Failed to load packages. {error}", path=error.path) | ||||
|  | ||||
|     return {"packages": packages} | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_UID | ||||
| from esphome.core import HexInt | ||||
| from . import pn532_ns, PN532, CONF_PN532_ID | ||||
|  | ||||
| from . import CONF_PN532_ID, PN532, pn532_ns | ||||
|  | ||||
| DEPENDENCIES = ["pn532"] | ||||
|  | ||||
| @@ -13,8 +14,7 @@ def validate_uid(value): | ||||
|     for x in value.split("-"): | ||||
|         if len(x) != 2: | ||||
|             raise cv.Invalid( | ||||
|                 "Each part (separated by '-') of the UID must be two characters " | ||||
|                 "long." | ||||
|                 "Each part (separated by '-') of the UID must be two characters long." | ||||
|             ) | ||||
|         try: | ||||
|             x = int(x, 16) | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import binary_sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_UID | ||||
| from esphome.core import HexInt | ||||
| from . import rc522_ns, RC522, CONF_RC522_ID | ||||
|  | ||||
| from . import CONF_RC522_ID, RC522, rc522_ns | ||||
|  | ||||
| DEPENDENCIES = ["rc522"] | ||||
|  | ||||
| @@ -13,8 +14,7 @@ def validate_uid(value): | ||||
|     for x in value.split("-"): | ||||
|         if len(x) != 2: | ||||
|             raise cv.Invalid( | ||||
|                 "Each part (separated by '-') of the UID must be two characters " | ||||
|                 "long." | ||||
|                 "Each part (separated by '-') of the UID must be two characters long." | ||||
|             ) | ||||
|         try: | ||||
|             x = int(x, 16) | ||||
|   | ||||
| @@ -46,6 +46,7 @@ class BSDSocketImpl : public Socket { | ||||
|       close();  // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) | ||||
|     } | ||||
|   } | ||||
|   int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(fd_, addr, addrlen); } | ||||
|   std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override { | ||||
|     int fd = ::accept(fd_, addr, addrlen); | ||||
|     if (fd == -1) | ||||
|   | ||||
| @@ -39,6 +39,7 @@ class LwIPSocketImpl : public Socket { | ||||
|       close();  // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) | ||||
|     } | ||||
|   } | ||||
|   int connect(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_connect(fd_, addr, addrlen); } | ||||
|   std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override { | ||||
|     int fd = lwip_accept(fd_, addr, addrlen); | ||||
|     if (fd == -1) | ||||
|   | ||||
| @@ -21,7 +21,9 @@ class Socket { | ||||
|   virtual int close() = 0; | ||||
|   // not supported yet: | ||||
|   // virtual int connect(const std::string &address) = 0; | ||||
|   // virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0; | ||||
| #if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) | ||||
|   virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0; | ||||
| #endif | ||||
|   virtual int shutdown(int how) = 0; | ||||
|  | ||||
|   virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; | ||||
|   | ||||
| @@ -1,15 +1,17 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import display | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_EXTERNAL_VCC, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_MODEL, | ||||
|     CONF_RESET_PIN, | ||||
|     CONF_BRIGHTNESS, | ||||
|     CONF_CONTRAST, | ||||
|     CONF_EXTERNAL_VCC, | ||||
|     CONF_INVERT, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_MODEL, | ||||
|     CONF_OFFSET_X, | ||||
|     CONF_OFFSET_Y, | ||||
|     CONF_RESET_PIN, | ||||
| ) | ||||
|  | ||||
| ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base") | ||||
| @@ -18,8 +20,6 @@ SSD1306Model = ssd1306_base_ns.enum("SSD1306Model") | ||||
|  | ||||
| CONF_FLIP_X = "flip_x" | ||||
| CONF_FLIP_Y = "flip_y" | ||||
| CONF_OFFSET_X = "offset_x" | ||||
| CONF_OFFSET_Y = "offset_y" | ||||
|  | ||||
| MODELS = { | ||||
|     "SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user