mirror of
				https://github.com/home-assistant/core.git
				synced 2025-10-30 22:19:37 +00:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			knx-moveou
			...
			mqtt-entit
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 4bb2c0b213 | ||
|   | 0307f0c781 | ||
|   | 81c4d8582a | ||
|   | f66a03d6cb | ||
|   | ff37570035 | ||
|   | 4f7e82ba76 | ||
|   | 87204bbfca | ||
|   | 69785a5361 | ||
|   | c566950fb1 | ||
|   | fd8e366a2f | ||
|   | ab658e05a6 | ||
|   | 49542e8302 | ||
|   | bc8d7fc02e | ||
|   | 20a494e4f8 | ||
|   | 64ad83b1cd | ||
|   | 254a4de025 | ||
|   | ec43e01d51 | ||
|   | 4f25518671 | 
| @@ -33,7 +33,7 @@ | ||||
|         "GitHub.vscode-pull-request-github", | ||||
|         "GitHub.copilot" | ||||
|       ], | ||||
|       // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.jsonc | ||||
|       // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json | ||||
|       "settings": { | ||||
|         "python.experiments.optOutFrom": ["pythonTestAdapter"], | ||||
|         "python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python", | ||||
| @@ -41,7 +41,6 @@ | ||||
|         "python.terminal.activateEnvInCurrentTerminal": true, | ||||
|         "python.testing.pytestArgs": ["--no-cov"], | ||||
|         "pylint.importStrategy": "fromEnvironment", | ||||
|         "python.analysis.typeCheckingMode": "basic", | ||||
|         "editor.formatOnPaste": false, | ||||
|         "editor.formatOnSave": true, | ||||
|         "editor.formatOnType": true, | ||||
| @@ -63,9 +62,6 @@ | ||||
|         "[python]": { | ||||
|           "editor.defaultFormatter": "charliermarsh.ruff" | ||||
|         }, | ||||
|         "[json][jsonc][yaml]": { | ||||
|           "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|         }, | ||||
|         "json.schemas": [ | ||||
|           { | ||||
|             "fileMatch": ["homeassistant/components/*/manifest.json"], | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/copilot-instructions.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/copilot-instructions.md
									
									
									
									
										vendored
									
									
								
							| @@ -74,7 +74,6 @@ rules: | ||||
| - **Formatting**: Ruff | ||||
| - **Linting**: PyLint and Ruff | ||||
| - **Type Checking**: MyPy | ||||
| - **Lint/Type/Format Fixes**: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding `# type: ignore`, or skipping a formatter. Treat suppressions and `noqa` comments as a last resort once no compliant fix exists | ||||
| - **Testing**: pytest with plain functions and fixtures | ||||
| - **Language**: American English for all code, comments, and documentation (use sentence case, including titles) | ||||
|  | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/builder.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/builder.yml
									
									
									
									
										vendored
									
									
								
							| @@ -69,7 +69,7 @@ jobs: | ||||
|         run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - | ||||
|  | ||||
|       - name: Upload translations | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: translations | ||||
|           path: translations.tar.gz | ||||
| @@ -175,7 +175,7 @@ jobs: | ||||
|           sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt | ||||
|  | ||||
|       - name: Download translations | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: translations | ||||
|  | ||||
| @@ -464,7 +464,7 @@ jobs: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|  | ||||
|       - name: Download translations | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: translations | ||||
|  | ||||
|   | ||||
							
								
								
									
										26
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -37,7 +37,7 @@ on: | ||||
|         type: boolean | ||||
|  | ||||
| env: | ||||
|   CACHE_VERSION: 1 | ||||
|   CACHE_VERSION: 9 | ||||
|   UV_CACHE_VERSION: 1 | ||||
|   MYPY_CACHE_VERSION: 1 | ||||
|   HA_SHORT_VERSION: "2025.11" | ||||
| @@ -428,7 +428,7 @@ jobs: | ||||
|     timeout-minutes: 60 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         python-version: &matrix-python ${{ fromJson(needs.info.outputs.python_versions) }} | ||||
|         python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} | ||||
|     steps: | ||||
|       - *checkout | ||||
|       - &setup-python-matrix | ||||
| @@ -514,7 +514,9 @@ jobs: | ||||
|         if: steps.cache-apt-check.outputs.cache-hit != 'true' | ||||
|         uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         with: | ||||
|           path: *path-apt-cache | ||||
|           path: | | ||||
|             ${{ env.APT_CACHE_DIR }} | ||||
|             ${{ env.APT_LIST_CACHE_DIR }} | ||||
|           key: *key-apt-cache | ||||
|       - name: Create Python virtual environment | ||||
|         if: steps.cache-venv.outputs.cache-hit != 'true' | ||||
| @@ -535,7 +537,7 @@ jobs: | ||||
|           python --version | ||||
|           uv pip freeze >> pip_freeze.txt | ||||
|       - name: Upload pip_freeze artifact | ||||
|         uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         uses: &actions-upload-artifact actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: pip-freeze-${{ matrix.python-version }} | ||||
|           path: pip_freeze.txt | ||||
| @@ -639,7 +641,7 @@ jobs: | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         python-version: *matrix-python | ||||
|         python-version: ${{ fromJson(needs.info.outputs.python_versions) }} | ||||
|     steps: | ||||
|       - *checkout | ||||
|       - *setup-python-matrix | ||||
| @@ -836,8 +838,8 @@ jobs: | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         python-version: *matrix-python | ||||
|         group: &matrix-group ${{ fromJson(needs.info.outputs.test_groups) }} | ||||
|         python-version: ${{ fromJson(needs.info.outputs.python_versions) }} | ||||
|         group: ${{ fromJson(needs.info.outputs.test_groups) }} | ||||
|     steps: | ||||
|       - *cache-restore-apt | ||||
|       - name: Install additional OS dependencies | ||||
| @@ -867,7 +869,7 @@ jobs: | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" | ||||
|       - name: Download pytest_buckets | ||||
|         uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         uses: &actions-download-artifact actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: pytest_buckets | ||||
|       - &compile-english-translations | ||||
| @@ -962,7 +964,7 @@ jobs: | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         python-version: *matrix-python | ||||
|         python-version: ${{ fromJson(needs.info.outputs.python_versions) }} | ||||
|         mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }} | ||||
|     steps: | ||||
|       - *cache-restore-apt | ||||
| @@ -1079,7 +1081,7 @@ jobs: | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         python-version: *matrix-python | ||||
|         python-version: ${{ fromJson(needs.info.outputs.python_versions) }} | ||||
|         postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }} | ||||
|     steps: | ||||
|       - *cache-restore-apt | ||||
| @@ -1216,8 +1218,8 @@ jobs: | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         python-version: *matrix-python | ||||
|         group: *matrix-group | ||||
|         python-version: ${{ fromJson(needs.info.outputs.python_versions) }} | ||||
|         group: ${{ fromJson(needs.info.outputs.test_groups) }} | ||||
|     steps: | ||||
|       - *cache-restore-apt | ||||
|       - name: Install additional OS dependencies | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							| @@ -24,11 +24,11 @@ jobs: | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - name: Initialize CodeQL | ||||
|         uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|         uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 | ||||
|         with: | ||||
|           languages: python | ||||
|  | ||||
|       - name: Perform CodeQL Analysis | ||||
|         uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|         uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 | ||||
|         with: | ||||
|           category: "/language:python" | ||||
|   | ||||
							
								
								
									
										78
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										78
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							| @@ -31,8 +31,7 @@ jobs: | ||||
|     outputs: | ||||
|       architectures: ${{ steps.info.outputs.architectures }} | ||||
|     steps: | ||||
|       - &checkout | ||||
|         name: Checkout the repository | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||
| @@ -92,7 +91,7 @@ jobs: | ||||
|           ) > build_constraints.txt | ||||
|  | ||||
|       - name: Upload env_file | ||||
|         uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: env_file | ||||
|           path: ./.env_file | ||||
| @@ -100,14 +99,14 @@ jobs: | ||||
|           overwrite: true | ||||
|  | ||||
|       - name: Upload build_constraints | ||||
|         uses: *actions-upload-artifact | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: build_constraints | ||||
|           path: ./build_constraints.txt | ||||
|           overwrite: true | ||||
|  | ||||
|       - name: Upload requirements_diff | ||||
|         uses: *actions-upload-artifact | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: requirements_diff | ||||
|           path: ./requirements_diff.txt | ||||
| @@ -119,7 +118,7 @@ jobs: | ||||
|           python -m script.gen_requirements_all ci | ||||
|  | ||||
|       - name: Upload requirements_all_wheels | ||||
|         uses: *actions-upload-artifact | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: requirements_all_wheels | ||||
|           path: ./requirements_all_wheels_*.txt | ||||
| @@ -128,41 +127,28 @@ jobs: | ||||
|     name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2) | ||||
|     if: github.repository_owner == 'home-assistant' | ||||
|     needs: init | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: &matrix-build | ||||
|         abi: ["cp313", "cp314"] | ||||
|       matrix: | ||||
|         abi: ["cp313"] | ||||
|         arch: ${{ fromJson(needs.init.outputs.architectures) }} | ||||
|         include: | ||||
|           - os: ubuntu-latest | ||||
|           - arch: aarch64 | ||||
|             os: ubuntu-24.04-arm | ||||
|         exclude: | ||||
|           - abi: cp314 | ||||
|             arch: armv7 | ||||
|           - abi: cp314 | ||||
|             arch: armhf | ||||
|           - abi: cp314 | ||||
|             arch: i386 | ||||
|     steps: | ||||
|       - *checkout | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - &download-env-file | ||||
|         name: Download env_file | ||||
|         uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|       - name: Download env_file | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: env_file | ||||
|  | ||||
|       - &download-build-constraints | ||||
|         name: Download build_constraints | ||||
|         uses: *actions-download-artifact | ||||
|       - name: Download build_constraints | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: build_constraints | ||||
|  | ||||
|       - &download-requirements-diff | ||||
|         name: Download requirements_diff | ||||
|         uses: *actions-download-artifact | ||||
|       - name: Download requirements_diff | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: requirements_diff | ||||
|  | ||||
| @@ -174,7 +160,7 @@ jobs: | ||||
|  | ||||
|       # home-assistant/wheels doesn't support sha pinning | ||||
|       - name: Build wheels | ||||
|         uses: &home-assistant-wheels home-assistant/wheels@2025.10.0 | ||||
|         uses: home-assistant/wheels@2025.09.1 | ||||
|         with: | ||||
|           abi: ${{ matrix.abi }} | ||||
|           tag: musllinux_1_2 | ||||
| @@ -191,19 +177,33 @@ jobs: | ||||
|     name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }} | ||||
|     if: github.repository_owner == 'home-assistant' | ||||
|     needs: init | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: *matrix-build | ||||
|       matrix: | ||||
|         abi: ["cp313"] | ||||
|         arch: ${{ fromJson(needs.init.outputs.architectures) }} | ||||
|     steps: | ||||
|       - *checkout | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - *download-env-file | ||||
|       - *download-build-constraints | ||||
|       - *download-requirements-diff | ||||
|       - name: Download env_file | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: env_file | ||||
|  | ||||
|       - name: Download build_constraints | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: build_constraints | ||||
|  | ||||
|       - name: Download requirements_diff | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: requirements_diff | ||||
|  | ||||
|       - name: Download requirements_all_wheels | ||||
|         uses: *actions-download-artifact | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: requirements_all_wheels | ||||
|  | ||||
| @@ -221,7 +221,7 @@ jobs: | ||||
|  | ||||
|       # home-assistant/wheels doesn't support sha pinning | ||||
|       - name: Build wheels | ||||
|         uses: *home-assistant-wheels | ||||
|         uses: home-assistant/wheels@2025.09.1 | ||||
|         with: | ||||
|           abi: ${{ matrix.abi }} | ||||
|           tag: musllinux_1_2 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -111,7 +111,6 @@ virtualization/vagrant/config | ||||
| !.vscode/cSpell.json | ||||
| !.vscode/extensions.json | ||||
| !.vscode/tasks.json | ||||
| !.vscode/settings.default.jsonc | ||||
| .env | ||||
|  | ||||
| # Windows Explorer | ||||
| @@ -141,5 +140,4 @@ pytest_buckets.txt | ||||
|  | ||||
| # AI tooling | ||||
| .claude/settings.local.json | ||||
| .serena/ | ||||
|  | ||||
|   | ||||
| @@ -182,6 +182,7 @@ homeassistant.components.efergy.* | ||||
| homeassistant.components.eheimdigital.* | ||||
| homeassistant.components.electrasmart.* | ||||
| homeassistant.components.electric_kiwi.* | ||||
| homeassistant.components.elevenlabs.* | ||||
| homeassistant.components.elgato.* | ||||
| homeassistant.components.elkm1.* | ||||
| homeassistant.components.emulated_hue.* | ||||
| @@ -278,7 +279,6 @@ homeassistant.components.imap.* | ||||
| homeassistant.components.imgw_pib.* | ||||
| homeassistant.components.immich.* | ||||
| homeassistant.components.incomfort.* | ||||
| homeassistant.components.inels.* | ||||
| homeassistant.components.input_button.* | ||||
| homeassistant.components.input_select.* | ||||
| homeassistant.components.input_text.* | ||||
| @@ -478,7 +478,6 @@ homeassistant.components.skybell.* | ||||
| homeassistant.components.slack.* | ||||
| homeassistant.components.sleep_as_android.* | ||||
| homeassistant.components.sleepiq.* | ||||
| homeassistant.components.sma.* | ||||
| homeassistant.components.smhi.* | ||||
| homeassistant.components.smlight.* | ||||
| homeassistant.components.smtp.* | ||||
|   | ||||
| @@ -7,19 +7,13 @@ | ||||
|   "python.testing.pytestEnabled": false, | ||||
|   // https://code.visualstudio.com/docs/python/linting#_general-settings | ||||
|   "pylint.importStrategy": "fromEnvironment", | ||||
|   // Pyright is too pedantic for Home Assistant | ||||
|   "python.analysis.typeCheckingMode": "basic", | ||||
|   "[python]": { | ||||
|     "editor.defaultFormatter": "charliermarsh.ruff" | ||||
|   }, | ||||
|   "[json][jsonc][yaml]": { | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|   }, | ||||
|   "json.schemas": [ | ||||
|     { | ||||
|       "fileMatch": ["homeassistant/components/*/manifest.json"], | ||||
|       // This value differs between working with devcontainer and locally, therefore this value should NOT be in sync! | ||||
|       "url": "./script/json_schemas/manifest_schema.json" | ||||
|     } | ||||
|   ] | ||||
|         { | ||||
|             "fileMatch": [ | ||||
|                 "homeassistant/components/*/manifest.json" | ||||
|             ], | ||||
|             // This value differs between working with devcontainer and locally, therefor this value should NOT be in sync! | ||||
|             "url": "./script/json_schemas/manifest_schema.json" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										6
									
								
								CODEOWNERS
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								CODEOWNERS
									
									
									
										generated
									
									
									
								
							| @@ -494,8 +494,6 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/filesize/ @gjohansson-ST | ||||
| /homeassistant/components/filter/ @dgomes | ||||
| /tests/components/filter/ @dgomes | ||||
| /homeassistant/components/fing/ @Lorenzo-Gasparini | ||||
| /tests/components/fing/ @Lorenzo-Gasparini | ||||
| /homeassistant/components/firefly_iii/ @erwindouna | ||||
| /tests/components/firefly_iii/ @erwindouna | ||||
| /homeassistant/components/fireservicerota/ @cyberjunky | ||||
| @@ -743,8 +741,6 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/improv_ble/ @emontnemery | ||||
| /homeassistant/components/incomfort/ @jbouwh | ||||
| /tests/components/incomfort/ @jbouwh | ||||
| /homeassistant/components/inels/ @epdevlab | ||||
| /tests/components/inels/ @epdevlab | ||||
| /homeassistant/components/influxdb/ @mdegat01 | ||||
| /tests/components/influxdb/ @mdegat01 | ||||
| /homeassistant/components/inkbird/ @bdraco | ||||
| @@ -1543,8 +1539,6 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/suez_water/ @ooii @jb101010-2 | ||||
| /homeassistant/components/sun/ @home-assistant/core | ||||
| /tests/components/sun/ @home-assistant/core | ||||
| /homeassistant/components/sunricher_dali_center/ @niracler | ||||
| /tests/components/sunricher_dali_center/ @niracler | ||||
| /homeassistant/components/supla/ @mwegrzynek | ||||
| /homeassistant/components/surepetcare/ @benleb @danielhiversen | ||||
| /tests/components/surepetcare/ @benleb @danielhiversen | ||||
|   | ||||
							
								
								
									
										4
									
								
								Dockerfile
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								Dockerfile
									
									
									
										generated
									
									
									
								
							| @@ -25,13 +25,13 @@ RUN \ | ||||
|         "armv7") go2rtc_suffix='arm' ;; \ | ||||
|         *) go2rtc_suffix=${BUILD_ARCH} ;; \ | ||||
|     esac \ | ||||
|     && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.11/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ | ||||
|     && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ | ||||
|     && chmod +x /bin/go2rtc \ | ||||
|     # Verify go2rtc can be executed | ||||
|     && go2rtc --version | ||||
|  | ||||
| # Install uv | ||||
| RUN pip3 install uv==0.9.5 | ||||
| RUN pip3 install uv==0.8.9 | ||||
|  | ||||
| WORKDIR /usr/src | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,9 @@ build_from: | ||||
|   armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1 | ||||
|   amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1 | ||||
|   i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1 | ||||
| codenotary: | ||||
|   signer: notary@home-assistant.io | ||||
|   base_image: notary@home-assistant.io | ||||
| cosign: | ||||
|   base_identity: https://github.com/home-assistant/docker/.* | ||||
|   identity: https://github.com/home-assistant/core/.* | ||||
|   | ||||
| @@ -34,9 +34,6 @@ INPUT_FIELD_CODE = "code" | ||||
|  | ||||
| DUMMY_SECRET = "FPPTH34D4E3MI2HG" | ||||
|  | ||||
| GOOGLE_AUTHENTICATOR_URL = "https://support.google.com/accounts/answer/1066447" | ||||
| AUTHY_URL = "https://authy.com/" | ||||
|  | ||||
|  | ||||
| def _generate_qr_code(data: str) -> str: | ||||
|     """Generate a base64 PNG string represent QR Code image of data.""" | ||||
| @@ -232,8 +229,6 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]): | ||||
|                 "code": self._ota_secret, | ||||
|                 "url": self._url, | ||||
|                 "qr_code": self._image, | ||||
|                 "google_authenticator_url": GOOGLE_AUTHENTICATOR_URL, | ||||
|                 "authy_url": AUTHY_URL, | ||||
|             }, | ||||
|             errors=errors, | ||||
|         ) | ||||
|   | ||||
| @@ -6,5 +6,5 @@ | ||||
|   "documentation": "https://www.home-assistant.io/integrations/adax", | ||||
|   "iot_class": "local_polling", | ||||
|   "loggers": ["adax", "adax_local"], | ||||
|   "requirements": ["adax==0.4.0", "Adax-local==0.2.0"] | ||||
|   "requirements": ["adax==0.4.0", "Adax-local==0.1.5"] | ||||
| } | ||||
|   | ||||
| @@ -53,6 +53,9 @@ __all__ = [ | ||||
|     "GenImageTaskResult", | ||||
|     "async_generate_data", | ||||
|     "async_generate_image", | ||||
|     "async_setup", | ||||
|     "async_setup_entry", | ||||
|     "async_unload_entry", | ||||
| ] | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ from __future__ import annotations | ||||
|  | ||||
| from datetime import timedelta | ||||
| import logging | ||||
| from typing import Final, final | ||||
| from typing import Any, Final, final | ||||
|  | ||||
| from homeassistant.config_entries import ConfigEntry | ||||
| from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER | ||||
| @@ -133,9 +133,9 @@ class AirQualityEntity(Entity): | ||||
|  | ||||
|     @final | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, str | int | float]: | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return the state attributes.""" | ||||
|         data: dict[str, str | int | float] = {} | ||||
|         data: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         for prop, attr in PROP_TO_ATTR.items(): | ||||
|             if (value := getattr(self, prop)) is not None: | ||||
|   | ||||
| @@ -26,10 +26,6 @@ from .const import DOMAIN | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| # Documentation URL for API key generation | ||||
| _API_KEY_URL = "https://docs.airnowapi.org/account/request/" | ||||
|  | ||||
|  | ||||
| async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: | ||||
|     """Validate the user input allows us to connect. | ||||
|  | ||||
| @@ -118,7 +114,6 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|             description_placeholders={"api_key_url": _API_KEY_URL}, | ||||
|             errors=errors, | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   "config": { | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "description": "To generate API key go to {api_key_url}", | ||||
|         "description": "To generate API key go to https://docs.airnowapi.org/account/request/", | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]", | ||||
|           "latitude": "[%key:common::config_flow::data::latitude%]", | ||||
|   | ||||
| @@ -7,5 +7,5 @@ | ||||
|   "integration_type": "device", | ||||
|   "iot_class": "local_polling", | ||||
|   "quality_scale": "silver", | ||||
|   "requirements": ["airos==0.6.0"] | ||||
|   "requirements": ["airos==0.5.6"] | ||||
| } | ||||
|   | ||||
| @@ -11,5 +11,5 @@ | ||||
|   "documentation": "https://www.home-assistant.io/integrations/airzone", | ||||
|   "iot_class": "local_polling", | ||||
|   "loggers": ["aioairzone"], | ||||
|   "requirements": ["aioairzone==1.0.2"] | ||||
|   "requirements": ["aioairzone==1.0.1"] | ||||
| } | ||||
|   | ||||
| @@ -301,11 +301,12 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, Any] | None: | ||||
|         """Return the state attributes.""" | ||||
|         return { | ||||
|             ATTR_CODE_FORMAT: self.code_format, | ||||
|             ATTR_CHANGED_BY: self.changed_by, | ||||
|             ATTR_CODE_ARM_REQUIRED: self.code_arm_required, | ||||
|         } | ||||
|         data: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         data[ATTR_CODE_FORMAT] = self.code_format | ||||
|         data[ATTR_CHANGED_BY] = self.changed_by | ||||
|         data[ATTR_CODE_ARM_REQUIRED] = self.code_arm_required | ||||
|         return data | ||||
|  | ||||
|     async def async_internal_added_to_hass(self) -> None: | ||||
|         """Call when the alarm control panel entity is added to hass.""" | ||||
|   | ||||
| @@ -8,5 +8,5 @@ | ||||
|   "iot_class": "cloud_polling", | ||||
|   "loggers": ["aioamazondevices"], | ||||
|   "quality_scale": "platinum", | ||||
|   "requirements": ["aioamazondevices==6.4.6"] | ||||
|   "requirements": ["aioamazondevices==6.4.4"] | ||||
| } | ||||
|   | ||||
| @@ -41,11 +41,6 @@ APPS_NEW_ID = "add_new" | ||||
| CONF_APP_DELETE = "app_delete" | ||||
| CONF_APP_ID = "app_id" | ||||
|  | ||||
| _EXAMPLE_APP_ID = "com.plexapp.android" | ||||
| _EXAMPLE_APP_PLAY_STORE_URL = ( | ||||
|     f"https://play.google.com/store/apps/details?id={_EXAMPLE_APP_ID}" | ||||
| ) | ||||
|  | ||||
| STEP_PAIR_DATA_SCHEMA = vol.Schema( | ||||
|     { | ||||
|         vol.Required("pin"): str, | ||||
| @@ -360,7 +355,5 @@ class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithReload): | ||||
|             data_schema=data_schema, | ||||
|             description_placeholders={ | ||||
|                 "app_id": f"`{app_id}`" if app_id != APPS_NEW_ID else "", | ||||
|                 "example_app_id": _EXAMPLE_APP_ID, | ||||
|                 "example_app_play_store_url": _EXAMPLE_APP_PLAY_STORE_URL, | ||||
|             }, | ||||
|         ) | ||||
|   | ||||
| @@ -75,7 +75,7 @@ | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "app_name": "Name of the application as you would like it to be displayed in Home Assistant.", | ||||
|           "app_id": "E.g. {example_app_id} for {example_app_play_store_url}", | ||||
|           "app_id": "E.g. com.plexapp.android for https://play.google.com/store/apps/details?id=com.plexapp.android", | ||||
|           "app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename", | ||||
|           "app_delete": "Check this box to delete the application from the list." | ||||
|         } | ||||
|   | ||||
| @@ -65,6 +65,7 @@ __all__ = ( | ||||
|     "async_create_default_pipeline", | ||||
|     "async_get_pipelines", | ||||
|     "async_pipeline_from_audio_stream", | ||||
|     "async_setup", | ||||
|     "async_update_pipeline", | ||||
| ) | ||||
|  | ||||
|   | ||||
| @@ -19,14 +19,7 @@ import wave | ||||
| import hass_nabucasa | ||||
| import voluptuous as vol | ||||
|  | ||||
| from homeassistant.components import ( | ||||
|     conversation, | ||||
|     media_player, | ||||
|     stt, | ||||
|     tts, | ||||
|     wake_word, | ||||
|     websocket_api, | ||||
| ) | ||||
| from homeassistant.components import conversation, stt, tts, wake_word, websocket_api | ||||
| from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL | ||||
| from homeassistant.core import Context, HomeAssistant, callback | ||||
| from homeassistant.exceptions import HomeAssistantError | ||||
| @@ -137,10 +130,7 @@ SAVE_DELAY = 10 | ||||
| @callback | ||||
| def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool: | ||||
|     """Filter out intents that are not local fallback.""" | ||||
|     return result.intent.name in ( | ||||
|         intent.INTENT_GET_STATE, | ||||
|         media_player.INTENT_MEDIA_SEARCH_AND_PLAY, | ||||
|     ) | ||||
|     return result.intent.name in (intent.INTENT_GET_STATE) | ||||
|  | ||||
|  | ||||
| @callback | ||||
|   | ||||
| @@ -125,9 +125,7 @@ class AsusWrtBridge(ABC): | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_bridge( | ||||
|         hass: HomeAssistant, | ||||
|         conf: dict[str, str | int], | ||||
|         options: dict[str, str | bool | int] | None = None, | ||||
|         hass: HomeAssistant, conf: dict[str, Any], options: dict[str, Any] | None = None | ||||
|     ) -> AsusWrtBridge: | ||||
|         """Get Bridge instance.""" | ||||
|         if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP): | ||||
|   | ||||
| @@ -175,12 +175,12 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|         ) | ||||
|  | ||||
|     async def _async_check_connection( | ||||
|         self, user_input: dict[str, str | int] | ||||
|         self, user_input: dict[str, Any] | ||||
|     ) -> tuple[str, str | None]: | ||||
|         """Attempt to connect the AsusWrt router.""" | ||||
|  | ||||
|         api: AsusWrtBridge | ||||
|         host = user_input[CONF_HOST] | ||||
|         host: str = user_input[CONF_HOST] | ||||
|         protocol = user_input[CONF_PROTOCOL] | ||||
|         error: str | None = None | ||||
|  | ||||
|   | ||||
| @@ -176,7 +176,7 @@ class AsusWrtRouter: | ||||
|  | ||||
|         self._on_close: list[Callable] = [] | ||||
|  | ||||
|         self._options: dict[str, str | bool | int] = { | ||||
|         self._options: dict[str, Any] = { | ||||
|             CONF_DNSMASQ: DEFAULT_DNSMASQ, | ||||
|             CONF_INTERFACE: DEFAULT_INTERFACE, | ||||
|             CONF_REQUIRE_IP: True, | ||||
| @@ -299,10 +299,12 @@ class AsusWrtRouter: | ||||
|             _LOGGER.warning("Reconnected to ASUS router %s", self.host) | ||||
|  | ||||
|         self._connected_devices = len(wrt_devices) | ||||
|         consider_home = int( | ||||
|             self._options.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()) | ||||
|         consider_home: int = self._options.get( | ||||
|             CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() | ||||
|         ) | ||||
|         track_unknown: bool = self._options.get( | ||||
|             CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN | ||||
|         ) | ||||
|         track_unknown = self._options.get(CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN) | ||||
|  | ||||
|         for device_mac, device in self._devices.items(): | ||||
|             dev_info = wrt_devices.pop(device_mac, None) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|       "step": { | ||||
|         "init": { | ||||
|           "title": "Set up two-factor authentication using TOTP", | ||||
|           "description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator]({google_authenticator_url}) or [Authy]({authy_url}).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**." | ||||
|           "description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**." | ||||
|         } | ||||
|       }, | ||||
|       "error": { | ||||
|   | ||||
| @@ -109,12 +109,12 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|  | ||||
|                 if self.source == SOURCE_REAUTH: | ||||
|                     self._abort_if_unique_id_mismatch() | ||||
|                     return self.async_update_and_abort( | ||||
|                     return self.async_update_reload_and_abort( | ||||
|                         self._get_reauth_entry(), data_updates=config | ||||
|                     ) | ||||
|                 if self.source == SOURCE_RECONFIGURE: | ||||
|                     self._abort_if_unique_id_mismatch() | ||||
|                     return self.async_update_and_abort( | ||||
|                     return self.async_update_reload_and_abort( | ||||
|                         self._get_reconfigure_entry(), data_updates=config | ||||
|                     ) | ||||
|                 self._abort_if_unique_id_configured() | ||||
| @@ -248,7 +248,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|         await self.async_set_unique_id(discovery_info[CONF_MAC]) | ||||
|  | ||||
|         self._abort_if_unique_id_configured( | ||||
|             updates={CONF_HOST: discovery_info[CONF_HOST]}, reload_on_update=False | ||||
|             updates={CONF_HOST: discovery_info[CONF_HOST]} | ||||
|         ) | ||||
|  | ||||
|         self.context.update( | ||||
|   | ||||
| @@ -8,5 +8,5 @@ | ||||
|   "iot_class": "cloud_polling", | ||||
|   "loggers": ["bring_api"], | ||||
|   "quality_scale": "platinum", | ||||
|   "requirements": ["bring-api==1.1.1"] | ||||
|   "requirements": ["bring-api==1.1.0"] | ||||
| } | ||||
|   | ||||
| @@ -20,5 +20,5 @@ | ||||
|   "dependencies": ["bluetooth_adapters"], | ||||
|   "documentation": "https://www.home-assistant.io/integrations/bthome", | ||||
|   "iot_class": "local_push", | ||||
|   "requirements": ["bthome-ble==3.15.0"] | ||||
|   "requirements": ["bthome-ble==3.14.2"] | ||||
| } | ||||
|   | ||||
| @@ -525,17 +525,18 @@ class CalendarEntity(Entity): | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, Any] | None: | ||||
|         """Return the entity state attributes.""" | ||||
|         if (event := self.event) is None: | ||||
|             return None | ||||
|         data: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         return { | ||||
|             "message": event.summary, | ||||
|             "all_day": event.all_day, | ||||
|             "start_time": event.start_datetime_local.strftime(DATE_STR_FORMAT), | ||||
|             "end_time": event.end_datetime_local.strftime(DATE_STR_FORMAT), | ||||
|             "location": event.location if event.location else "", | ||||
|             "description": event.description if event.description else "", | ||||
|         } | ||||
|         if (event := self.event) is None: | ||||
|             return data or None | ||||
|  | ||||
|         data["message"] = event.summary | ||||
|         data["all_day"] = event.all_day | ||||
|         data["start_time"] = event.start_datetime_local.strftime(DATE_STR_FORMAT) | ||||
|         data["end_time"] = event.end_datetime_local.strftime(DATE_STR_FORMAT) | ||||
|         data["location"] = event.location if event.location else "" | ||||
|         data["description"] = event.description if event.description else "" | ||||
|         return data | ||||
|  | ||||
|     @final | ||||
|     @property | ||||
|   | ||||
| @@ -664,7 +664,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, str | None]: | ||||
|         """Return the camera state attributes.""" | ||||
|         attrs = {"access_token": self.access_tokens[-1]} | ||||
|         attrs: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         attrs["access_token"] = self.access_tokens[-1] | ||||
|  | ||||
|         if model := self.model: | ||||
|             attrs["model_name"] = model | ||||
|   | ||||
| @@ -816,20 +816,13 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): | ||||
|                 return MediaPlayerState.PAUSED | ||||
|             if media_status.player_is_idle: | ||||
|                 return MediaPlayerState.IDLE | ||||
|  | ||||
|         if self._chromecast is not None and self._chromecast.is_idle: | ||||
|             # If library consider us idle, that is our off state | ||||
|             # it takes HDMI status into account for cast devices. | ||||
|             return MediaPlayerState.OFF | ||||
|  | ||||
|         if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO: | ||||
|             # Some apps don't report media status, show the player as playing | ||||
|             return MediaPlayerState.PLAYING | ||||
|  | ||||
|         if self.app_id is not None: | ||||
|             # We have an active app | ||||
|         if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID: | ||||
|             if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO: | ||||
|                 # Some apps don't report media status, show the player as playing | ||||
|                 return MediaPlayerState.PLAYING | ||||
|             return MediaPlayerState.IDLE | ||||
|  | ||||
|         if self._chromecast is not None and self._chromecast.is_idle: | ||||
|             return MediaPlayerState.OFF | ||||
|         return None | ||||
|  | ||||
|     @property | ||||
|   | ||||
| @@ -14,7 +14,6 @@ from homeassistant.const import CONF_HOST, CONF_PORT | ||||
| from .const import DEFAULT_PORT, DOMAIN | ||||
| from .errors import ( | ||||
|     ConnectionRefused, | ||||
|     ConnectionReset, | ||||
|     ConnectionTimeout, | ||||
|     ResolveFailed, | ||||
|     ValidationFailure, | ||||
| @@ -50,8 +49,6 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|             self._errors[CONF_HOST] = "connection_timeout" | ||||
|         except ConnectionRefused: | ||||
|             self._errors[CONF_HOST] = "connection_refused" | ||||
|         except ConnectionReset: | ||||
|             self._errors[CONF_HOST] = "connection_reset" | ||||
|         except ValidationFailure: | ||||
|             return True | ||||
|         else: | ||||
|   | ||||
| @@ -25,7 +25,3 @@ class ConnectionTimeout(TemporaryFailure): | ||||
|  | ||||
| class ConnectionRefused(TemporaryFailure): | ||||
|     """Network connection refused.""" | ||||
|  | ||||
|  | ||||
| class ConnectionReset(TemporaryFailure): | ||||
|     """Network connection reset.""" | ||||
|   | ||||
| @@ -13,7 +13,6 @@ from homeassistant.util.ssl import get_default_context | ||||
| from .const import TIMEOUT | ||||
| from .errors import ( | ||||
|     ConnectionRefused, | ||||
|     ConnectionReset, | ||||
|     ConnectionTimeout, | ||||
|     ResolveFailed, | ||||
|     ValidationFailure, | ||||
| @@ -59,8 +58,6 @@ async def get_cert_expiry_timestamp( | ||||
|         raise ConnectionRefused( | ||||
|             f"Connection refused by server: {hostname}:{port}" | ||||
|         ) from err | ||||
|     except ConnectionResetError as err: | ||||
|         raise ConnectionReset(f"Connection reset by server: {hostname}:{port}") from err | ||||
|     except ssl.CertificateError as err: | ||||
|         raise ValidationFailure(err.verify_message) from err | ||||
|     except ssl.SSLError as err: | ||||
|   | ||||
| @@ -14,8 +14,7 @@ | ||||
|     "error": { | ||||
|       "resolve_failed": "This host cannot be resolved", | ||||
|       "connection_timeout": "Timeout when connecting to this host", | ||||
|       "connection_refused": "Connection refused when connecting to host", | ||||
|       "connection_reset": "Connection reset when connecting to host" | ||||
|       "connection_refused": "Connection refused when connecting to host" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", | ||||
|   | ||||
| @@ -341,16 +341,16 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return the optional state attributes.""" | ||||
|         data: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         supported_features = self.supported_features | ||||
|         temperature_unit = self.temperature_unit | ||||
|         precision = self.precision | ||||
|         hass = self.hass | ||||
|  | ||||
|         data: dict[str, str | float | None] = { | ||||
|             ATTR_CURRENT_TEMPERATURE: show_temp( | ||||
|                 hass, self.current_temperature, temperature_unit, precision | ||||
|             ), | ||||
|         } | ||||
|         data[ATTR_CURRENT_TEMPERATURE] = show_temp( | ||||
|             hass, self.current_temperature, temperature_unit, precision | ||||
|         ) | ||||
|  | ||||
|         if ClimateEntityFeature.TARGET_TEMPERATURE in supported_features: | ||||
|             data[ATTR_TEMPERATURE] = show_temp( | ||||
|   | ||||
| @@ -78,10 +78,7 @@ class CompitConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|                     ) | ||||
|  | ||||
|         return self.async_show_form( | ||||
|             step_id="user", | ||||
|             data_schema=STEP_USER_DATA_SCHEMA, | ||||
|             errors=errors, | ||||
|             description_placeholders={"compit_url": "https://inext.compit.pl/"}, | ||||
|             step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors | ||||
|         ) | ||||
|  | ||||
|     async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult: | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   "config": { | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "description": "Please enter your {compit_url} credentials.", | ||||
|         "description": "Please enter your https://inext.compit.pl/ credentials.", | ||||
|         "title": "Connect to Compit iNext", | ||||
|         "data": { | ||||
|           "email": "[%key:common::config_flow::data::email%]", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ from .const import ( | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| PLATFORMS = [Platform.CLIMATE, Platform.LIGHT, Platform.MEDIA_PLAYER] | ||||
| PLATFORMS = [Platform.LIGHT, Platform.MEDIA_PLAYER] | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
|   | ||||
| @@ -1,301 +0,0 @@ | ||||
| """Platform for Control4 Climate/Thermostat.""" | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| from datetime import timedelta | ||||
| import logging | ||||
| from typing import Any | ||||
|  | ||||
| from pyControl4.climate import C4Climate | ||||
| from pyControl4.error_handling import C4Exception | ||||
|  | ||||
| from homeassistant.components.climate import ( | ||||
|     ATTR_TARGET_TEMP_HIGH, | ||||
|     ATTR_TARGET_TEMP_LOW, | ||||
|     ClimateEntity, | ||||
|     ClimateEntityFeature, | ||||
|     HVACAction, | ||||
|     HVACMode, | ||||
| ) | ||||
| from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||||
|  | ||||
| from . import Control4ConfigEntry, Control4RuntimeData, get_items_of_category | ||||
| from .const import CONTROL4_ENTITY_TYPE | ||||
| from .director_utils import update_variables_for_config_entry | ||||
| from .entity import Control4Entity | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| CONTROL4_CATEGORY = "comfort" | ||||
|  | ||||
| # Control4 variable names | ||||
| CONTROL4_HVAC_STATE = "HVAC_STATE" | ||||
| CONTROL4_HVAC_MODE = "HVAC_MODE" | ||||
| CONTROL4_CURRENT_TEMPERATURE = "TEMPERATURE_F" | ||||
| CONTROL4_HUMIDITY = "HUMIDITY" | ||||
| CONTROL4_COOL_SETPOINT = "COOL_SETPOINT_F" | ||||
| CONTROL4_HEAT_SETPOINT = "HEAT_SETPOINT_F" | ||||
|  | ||||
| VARIABLES_OF_INTEREST = { | ||||
|     CONTROL4_HVAC_STATE, | ||||
|     CONTROL4_HVAC_MODE, | ||||
|     CONTROL4_CURRENT_TEMPERATURE, | ||||
|     CONTROL4_HUMIDITY, | ||||
|     CONTROL4_COOL_SETPOINT, | ||||
|     CONTROL4_HEAT_SETPOINT, | ||||
| } | ||||
|  | ||||
| # Map Control4 HVAC modes to Home Assistant | ||||
| C4_TO_HA_HVAC_MODE = { | ||||
|     "Off": HVACMode.OFF, | ||||
|     "Cool": HVACMode.COOL, | ||||
|     "Heat": HVACMode.HEAT, | ||||
|     "Auto": HVACMode.HEAT_COOL, | ||||
| } | ||||
|  | ||||
| HA_TO_C4_HVAC_MODE = {v: k for k, v in C4_TO_HA_HVAC_MODE.items()} | ||||
|  | ||||
| # Map Control4 HVAC state to Home Assistant HVAC action | ||||
| C4_TO_HA_HVAC_ACTION = { | ||||
|     "heating": HVACAction.HEATING, | ||||
|     "cooling": HVACAction.COOLING, | ||||
|     "idle": HVACAction.IDLE, | ||||
|     "off": HVACAction.OFF, | ||||
| } | ||||
|  | ||||
|  | ||||
| async def async_setup_entry( | ||||
|     hass: HomeAssistant, | ||||
|     entry: Control4ConfigEntry, | ||||
|     async_add_entities: AddConfigEntryEntitiesCallback, | ||||
| ) -> None: | ||||
|     """Set up Control4 thermostats from a config entry.""" | ||||
|     runtime_data = entry.runtime_data | ||||
|  | ||||
|     async def async_update_data() -> dict[int, dict[str, Any]]: | ||||
|         """Fetch data from Control4 director for thermostats.""" | ||||
|         try: | ||||
|             return await update_variables_for_config_entry( | ||||
|                 hass, entry, VARIABLES_OF_INTEREST | ||||
|             ) | ||||
|         except C4Exception as err: | ||||
|             raise UpdateFailed(f"Error communicating with API: {err}") from err | ||||
|  | ||||
|     coordinator = DataUpdateCoordinator[dict[int, dict[str, Any]]]( | ||||
|         hass, | ||||
|         _LOGGER, | ||||
|         name="climate", | ||||
|         update_method=async_update_data, | ||||
|         update_interval=timedelta(seconds=runtime_data.scan_interval), | ||||
|         config_entry=entry, | ||||
|     ) | ||||
|  | ||||
|     # Fetch initial data so we have data when entities subscribe | ||||
|     await coordinator.async_refresh() | ||||
|  | ||||
|     items_of_category = await get_items_of_category(hass, entry, CONTROL4_CATEGORY) | ||||
|     entity_list = [] | ||||
|     for item in items_of_category: | ||||
|         try: | ||||
|             if item["type"] == CONTROL4_ENTITY_TYPE: | ||||
|                 item_name = item["name"] | ||||
|                 item_id = item["id"] | ||||
|                 item_parent_id = item["parentId"] | ||||
|                 item_manufacturer = None | ||||
|                 item_device_name = None | ||||
|                 item_model = None | ||||
|  | ||||
|                 for parent_item in items_of_category: | ||||
|                     if parent_item["id"] == item_parent_id: | ||||
|                         item_manufacturer = parent_item.get("manufacturer") | ||||
|                         item_device_name = parent_item.get("roomName") | ||||
|                         item_model = parent_item.get("model") | ||||
|             else: | ||||
|                 continue | ||||
|         except KeyError: | ||||
|             _LOGGER.exception( | ||||
|                 "Unknown device properties received from Control4: %s", | ||||
|                 item, | ||||
|             ) | ||||
|             continue | ||||
|  | ||||
|         # Skip if we don't have data for this thermostat | ||||
|         if item_id not in coordinator.data: | ||||
|             _LOGGER.warning( | ||||
|                 "Couldn't get climate state data for %s (ID: %s), skipping setup", | ||||
|                 item_name, | ||||
|                 item_id, | ||||
|             ) | ||||
|             continue | ||||
|  | ||||
|         entity_list.append( | ||||
|             Control4Climate( | ||||
|                 runtime_data, | ||||
|                 coordinator, | ||||
|                 item_name, | ||||
|                 item_id, | ||||
|                 item_device_name, | ||||
|                 item_manufacturer, | ||||
|                 item_model, | ||||
|                 item_parent_id, | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     async_add_entities(entity_list) | ||||
|  | ||||
|  | ||||
| class Control4Climate(Control4Entity, ClimateEntity): | ||||
|     """Control4 climate entity.""" | ||||
|  | ||||
|     _attr_has_entity_name = True | ||||
|     _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT | ||||
|     _attr_supported_features = ( | ||||
|         ClimateEntityFeature.TARGET_TEMPERATURE | ||||
|         | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ||||
|         | ClimateEntityFeature.TURN_ON | ||||
|         | ClimateEntityFeature.TURN_OFF | ||||
|     ) | ||||
|     _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL] | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         runtime_data: Control4RuntimeData, | ||||
|         coordinator: DataUpdateCoordinator[dict[int, dict[str, Any]]], | ||||
|         name: str, | ||||
|         idx: int, | ||||
|         device_name: str | None, | ||||
|         device_manufacturer: str | None, | ||||
|         device_model: str | None, | ||||
|         device_id: int, | ||||
|     ) -> None: | ||||
|         """Initialize Control4 climate entity.""" | ||||
|         super().__init__( | ||||
|             runtime_data, | ||||
|             coordinator, | ||||
|             name, | ||||
|             idx, | ||||
|             device_name, | ||||
|             device_manufacturer, | ||||
|             device_model, | ||||
|             device_id, | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def available(self) -> bool: | ||||
|         """Return if entity is available.""" | ||||
|         return super().available and self._thermostat_data is not None | ||||
|  | ||||
|     def _create_api_object(self) -> C4Climate: | ||||
|         """Create a pyControl4 device object. | ||||
|  | ||||
|         This exists so the director token used is always the latest one, without needing to re-init the entire entity. | ||||
|         """ | ||||
|         return C4Climate(self.runtime_data.director, self._idx) | ||||
|  | ||||
|     @property | ||||
|     def _thermostat_data(self) -> dict[str, Any] | None: | ||||
|         """Return the thermostat data from the coordinator.""" | ||||
|         return self.coordinator.data.get(self._idx) | ||||
|  | ||||
|     @property | ||||
|     def current_temperature(self) -> float | None: | ||||
|         """Return the current temperature.""" | ||||
|         data = self._thermostat_data | ||||
|         if data is None: | ||||
|             return None | ||||
|         return data.get(CONTROL4_CURRENT_TEMPERATURE) | ||||
|  | ||||
|     @property | ||||
|     def current_humidity(self) -> int | None: | ||||
|         """Return the current humidity.""" | ||||
|         data = self._thermostat_data | ||||
|         if data is None: | ||||
|             return None | ||||
|         humidity = data.get(CONTROL4_HUMIDITY) | ||||
|         return int(humidity) if humidity is not None else None | ||||
|  | ||||
|     @property | ||||
|     def hvac_mode(self) -> HVACMode: | ||||
|         """Return current HVAC mode.""" | ||||
|         data = self._thermostat_data | ||||
|         if data is None: | ||||
|             return HVACMode.OFF | ||||
|         c4_mode = data.get(CONTROL4_HVAC_MODE) or "" | ||||
|         return C4_TO_HA_HVAC_MODE.get(c4_mode, HVACMode.OFF) | ||||
|  | ||||
|     @property | ||||
|     def hvac_action(self) -> HVACAction | None: | ||||
|         """Return current HVAC action.""" | ||||
|         data = self._thermostat_data | ||||
|         if data is None: | ||||
|             return None | ||||
|         c4_state = data.get(CONTROL4_HVAC_STATE) | ||||
|         if c4_state is None: | ||||
|             return None | ||||
|         # Convert state to lowercase for mapping | ||||
|         return C4_TO_HA_HVAC_ACTION.get(str(c4_state).lower()) | ||||
|  | ||||
|     @property | ||||
|     def target_temperature(self) -> float | None: | ||||
|         """Return the target temperature.""" | ||||
|         data = self._thermostat_data | ||||
|         if data is None: | ||||
|             return None | ||||
|         hvac_mode = self.hvac_mode | ||||
|         if hvac_mode == HVACMode.COOL: | ||||
|             return data.get(CONTROL4_COOL_SETPOINT) | ||||
|         if hvac_mode == HVACMode.HEAT: | ||||
|             return data.get(CONTROL4_HEAT_SETPOINT) | ||||
|         return None | ||||
|  | ||||
|     @property | ||||
|     def target_temperature_high(self) -> float | None: | ||||
|         """Return the high target temperature for auto mode.""" | ||||
|         data = self._thermostat_data | ||||
|         if data is None: | ||||
|             return None | ||||
|         if self.hvac_mode == HVACMode.HEAT_COOL: | ||||
|             return data.get(CONTROL4_COOL_SETPOINT) | ||||
|         return None | ||||
|  | ||||
|     @property | ||||
|     def target_temperature_low(self) -> float | None: | ||||
|         """Return the low target temperature for auto mode.""" | ||||
|         data = self._thermostat_data | ||||
|         if data is None: | ||||
|             return None | ||||
|         if self.hvac_mode == HVACMode.HEAT_COOL: | ||||
|             return data.get(CONTROL4_HEAT_SETPOINT) | ||||
|         return None | ||||
|  | ||||
|     async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: | ||||
|         """Set new target HVAC mode.""" | ||||
|         c4_hvac_mode = HA_TO_C4_HVAC_MODE[hvac_mode] | ||||
|         c4_climate = self._create_api_object() | ||||
|         await c4_climate.setHvacMode(c4_hvac_mode) | ||||
|         await self.coordinator.async_request_refresh() | ||||
|  | ||||
|     async def async_set_temperature(self, **kwargs: Any) -> None: | ||||
|         """Set new target temperature.""" | ||||
|         c4_climate = self._create_api_object() | ||||
|         low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) | ||||
|         high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) | ||||
|         temp = kwargs.get(ATTR_TEMPERATURE) | ||||
|  | ||||
|         # Handle temperature range for auto mode | ||||
|         if self.hvac_mode == HVACMode.HEAT_COOL: | ||||
|             if low_temp is not None: | ||||
|                 await c4_climate.setHeatSetpointF(low_temp) | ||||
|             if high_temp is not None: | ||||
|                 await c4_climate.setCoolSetpointF(high_temp) | ||||
|         # Handle single temperature setpoint | ||||
|         elif temp is not None: | ||||
|             if self.hvac_mode == HVACMode.COOL: | ||||
|                 await c4_climate.setCoolSetpointF(temp) | ||||
|             elif self.hvac_mode == HVACMode.HEAT: | ||||
|                 await c4_climate.setHeatSetpointF(temp) | ||||
|  | ||||
|         await self.coordinator.async_request_refresh() | ||||
| @@ -87,6 +87,7 @@ __all__ = [ | ||||
|     "async_get_chat_log", | ||||
|     "async_get_result_from_chat_log", | ||||
|     "async_set_agent", | ||||
|     "async_setup", | ||||
|     "async_unset_agent", | ||||
| ] | ||||
|  | ||||
|   | ||||
| @@ -267,7 +267,7 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return the state attributes.""" | ||||
|         data = {} | ||||
|         data: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         if (current := self.current_cover_position) is not None: | ||||
|             data[ATTR_CURRENT_POSITION] = current | ||||
|   | ||||
| @@ -7,5 +7,5 @@ | ||||
|   "integration_type": "hub", | ||||
|   "iot_class": "cloud_push", | ||||
|   "quality_scale": "bronze", | ||||
|   "requirements": ["pycync==0.4.2"] | ||||
|   "requirements": ["pycync==0.4.1"] | ||||
| } | ||||
|   | ||||
| @@ -184,8 +184,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|                             CONF_HOST: self.host, | ||||
|                             CONF_PORT: self.port, | ||||
|                             CONF_API_KEY: self.api_key, | ||||
|                         }, | ||||
|                         reload_on_update=False, | ||||
|                         } | ||||
|                     ) | ||||
|  | ||||
|             except TimeoutError: | ||||
| @@ -232,8 +231,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|             updates={ | ||||
|                 CONF_HOST: self.host, | ||||
|                 CONF_PORT: self.port, | ||||
|             }, | ||||
|             reload_on_update=False, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         self.context.update( | ||||
| @@ -267,8 +265,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|                 CONF_HOST: self.host, | ||||
|                 CONF_PORT: self.port, | ||||
|                 CONF_API_KEY: self.api_key, | ||||
|             }, | ||||
|             reload_on_update=False, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         self.context["configuration_url"] = HASSIO_CONFIGURATION_URL | ||||
|   | ||||
| @@ -3,14 +3,12 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| from datetime import datetime | ||||
| from typing import Any | ||||
|  | ||||
| from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState | ||||
| from homeassistant.config_entries import ConfigEntry | ||||
| from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||
| from homeassistant.helpers.event import async_track_utc_time_change | ||||
|  | ||||
| OPEN_CLOSE_DELAY = 2  # Used to give a realistic open/close experience in frontend | ||||
|  | ||||
| @@ -25,8 +23,6 @@ async def async_setup_entry( | ||||
|         [ | ||||
|             DemoValve("Front Garden", ValveState.OPEN), | ||||
|             DemoValve("Orchard", ValveState.CLOSED), | ||||
|             DemoValve("Back Garden", ValveState.CLOSED, position=70), | ||||
|             DemoValve("Trees", ValveState.CLOSED, position=30), | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
| @@ -41,7 +37,6 @@ class DemoValve(ValveEntity): | ||||
|         name: str, | ||||
|         state: str, | ||||
|         moveable: bool = True, | ||||
|         position: int | None = None, | ||||
|     ) -> None: | ||||
|         """Initialize the valve.""" | ||||
|         self._attr_name = name | ||||
| @@ -51,23 +46,11 @@ class DemoValve(ValveEntity): | ||||
|             ) | ||||
|         self._state = state | ||||
|         self._moveable = moveable | ||||
|         self._attr_reports_position = False | ||||
|         self._unsub_listener_valve: CALLBACK_TYPE | None = None | ||||
|         self._set_position: int = 0 | ||||
|         self._position: int = 0 | ||||
|         if position is None: | ||||
|             return | ||||
|  | ||||
|         self._position = self._set_position = position | ||||
|         self._attr_reports_position = True | ||||
|         self._attr_supported_features |= ( | ||||
|             ValveEntityFeature.SET_POSITION | ValveEntityFeature.STOP | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def current_valve_position(self) -> int: | ||||
|         """Return current position of valve.""" | ||||
|         return self._position | ||||
|     def is_open(self) -> bool: | ||||
|         """Return true if valve is open.""" | ||||
|         return self._state == ValveState.OPEN | ||||
|  | ||||
|     @property | ||||
|     def is_opening(self) -> bool: | ||||
| @@ -84,6 +67,11 @@ class DemoValve(ValveEntity): | ||||
|         """Return true if valve is closed.""" | ||||
|         return self._state == ValveState.CLOSED | ||||
|  | ||||
|     @property | ||||
|     def reports_position(self) -> bool: | ||||
|         """Return True if entity reports position, False otherwise.""" | ||||
|         return False | ||||
|  | ||||
|     async def async_open_valve(self, **kwargs: Any) -> None: | ||||
|         """Open the valve.""" | ||||
|         self._state = ValveState.OPENING | ||||
| @@ -99,45 +87,3 @@ class DemoValve(ValveEntity): | ||||
|         await asyncio.sleep(OPEN_CLOSE_DELAY) | ||||
|         self._state = ValveState.CLOSED | ||||
|         self.async_write_ha_state() | ||||
|  | ||||
|     async def async_stop_valve(self) -> None: | ||||
|         """Stop the valve.""" | ||||
|         self._state = ValveState.OPEN if self._position > 0 else ValveState.CLOSED | ||||
|         if self._unsub_listener_valve is not None: | ||||
|             self._unsub_listener_valve() | ||||
|             self._unsub_listener_valve = None | ||||
|         self.async_write_ha_state() | ||||
|  | ||||
|     async def async_set_valve_position(self, position: int) -> None: | ||||
|         """Move the valve to a specific position.""" | ||||
|         if position == self._position: | ||||
|             return | ||||
|         if position > self._position: | ||||
|             self._state = ValveState.OPENING | ||||
|         else: | ||||
|             self._state = ValveState.CLOSING | ||||
|  | ||||
|         self._set_position = round(position, -1) | ||||
|         self._listen_valve() | ||||
|         self.async_write_ha_state() | ||||
|  | ||||
|     @callback | ||||
|     def _listen_valve(self) -> None: | ||||
|         """Listen for changes in valve.""" | ||||
|         if self._unsub_listener_valve is None: | ||||
|             self._unsub_listener_valve = async_track_utc_time_change( | ||||
|                 self.hass, self._time_changed_valve | ||||
|             ) | ||||
|  | ||||
|     async def _time_changed_valve(self, now: datetime) -> None: | ||||
|         """Track time changes.""" | ||||
|         if self._state == ValveState.OPENING: | ||||
|             self._position += 10 | ||||
|         elif self._state == ValveState.CLOSING: | ||||
|             self._position -= 10 | ||||
|  | ||||
|         if self._position in (100, 0, self._set_position): | ||||
|             await self.async_stop_valve() | ||||
|             return | ||||
|  | ||||
|         self.async_write_ha_state() | ||||
|   | ||||
| @@ -2,12 +2,12 @@ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME  # noqa: F401 | ||||
| from homeassistant.const import STATE_HOME | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.typing import ConfigType | ||||
| from homeassistant.loader import bind_hass | ||||
|  | ||||
| from .config_entry import (  # noqa: F401 | ||||
| from .config_entry import ( | ||||
|     ScannerEntity, | ||||
|     ScannerEntityDescription, | ||||
|     TrackerEntity, | ||||
| @@ -15,7 +15,7 @@ from .config_entry import (  # noqa: F401 | ||||
|     async_setup_entry, | ||||
|     async_unload_entry, | ||||
| ) | ||||
| from .const import (  # noqa: F401 | ||||
| from .const import ( | ||||
|     ATTR_ATTRIBUTES, | ||||
|     ATTR_BATTERY, | ||||
|     ATTR_DEV_ID, | ||||
| @@ -37,7 +37,7 @@ from .const import (  # noqa: F401 | ||||
|     SCAN_INTERVAL, | ||||
|     SourceType, | ||||
| ) | ||||
| from .legacy import (  # noqa: F401 | ||||
| from .legacy import ( | ||||
|     PLATFORM_SCHEMA, | ||||
|     PLATFORM_SCHEMA_BASE, | ||||
|     SERVICE_SEE, | ||||
| @@ -61,3 +61,44 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: | ||||
|     """Set up the device tracker.""" | ||||
|     async_setup_legacy_integration(hass, config) | ||||
|     return True | ||||
|  | ||||
|  | ||||
| __all__ = ( | ||||
|     "ATTR_ATTRIBUTES", | ||||
|     "ATTR_BATTERY", | ||||
|     "ATTR_DEV_ID", | ||||
|     "ATTR_GPS", | ||||
|     "ATTR_HOST_NAME", | ||||
|     "ATTR_IP", | ||||
|     "ATTR_LOCATION_NAME", | ||||
|     "ATTR_MAC", | ||||
|     "ATTR_SOURCE_TYPE", | ||||
|     "CONF_CONSIDER_HOME", | ||||
|     "CONF_NEW_DEVICE_DEFAULTS", | ||||
|     "CONF_SCAN_INTERVAL", | ||||
|     "CONF_TRACK_NEW", | ||||
|     "CONNECTED_DEVICE_REGISTERED", | ||||
|     "DEFAULT_CONSIDER_HOME", | ||||
|     "DEFAULT_TRACK_NEW", | ||||
|     "DOMAIN", | ||||
|     "ENTITY_ID_FORMAT", | ||||
|     "PLATFORM_SCHEMA", | ||||
|     "PLATFORM_SCHEMA_BASE", | ||||
|     "SCAN_INTERVAL", | ||||
|     "SERVICE_SEE", | ||||
|     "SERVICE_SEE_PAYLOAD_SCHEMA", | ||||
|     "SOURCE_TYPES", | ||||
|     "AsyncSeeCallback", | ||||
|     "DeviceScanner", | ||||
|     "ScannerEntity", | ||||
|     "ScannerEntityDescription", | ||||
|     "SeeCallback", | ||||
|     "SourceType", | ||||
|     "TrackerEntity", | ||||
|     "TrackerEntityDescription", | ||||
|     "async_setup", | ||||
|     "async_setup_entry", | ||||
|     "async_unload_entry", | ||||
|     "is_on", | ||||
|     "see", | ||||
| ) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| from typing import final | ||||
| from typing import Any, final | ||||
|  | ||||
| from propcache.api import cached_property | ||||
|  | ||||
| @@ -28,7 +28,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send | ||||
| from homeassistant.helpers.entity import Entity, EntityDescription | ||||
| from homeassistant.helpers.entity_component import EntityComponent | ||||
| from homeassistant.helpers.entity_platform import EntityPlatform | ||||
| from homeassistant.helpers.typing import StateType | ||||
| from homeassistant.util.hass_dict import HassKey | ||||
|  | ||||
| from .const import ( | ||||
| @@ -189,9 +188,11 @@ class BaseTrackerEntity(Entity): | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, StateType]: | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return the device state attributes.""" | ||||
|         attr: dict[str, StateType] = {ATTR_SOURCE_TYPE: self.source_type} | ||||
|         attr: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         attr[ATTR_SOURCE_TYPE] = self.source_type | ||||
|  | ||||
|         if self.battery_level is not None: | ||||
|             attr[ATTR_BATTERY_LEVEL] = self.battery_level | ||||
| @@ -278,9 +279,9 @@ class TrackerEntity( | ||||
|  | ||||
|     @final | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, StateType]: | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return the device state attributes.""" | ||||
|         attr: dict[str, StateType] = {} | ||||
|         attr: dict[str, Any] = {} | ||||
|         attr.update(super().state_attributes) | ||||
|  | ||||
|         if self.latitude is not None and self.longitude is not None: | ||||
| @@ -431,9 +432,10 @@ class ScannerEntity( | ||||
|  | ||||
|     @final | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, StateType]: | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return the device state attributes.""" | ||||
|         attr = super().state_attributes | ||||
|         attr: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|         attr.update(super().state_attributes) | ||||
|  | ||||
|         if ip_address := self.ip_address: | ||||
|             attr[ATTR_IP] = ip_address | ||||
|   | ||||
| @@ -48,7 +48,7 @@ from homeassistant.helpers.event import ( | ||||
|     async_track_utc_time_change, | ||||
| ) | ||||
| from homeassistant.helpers.restore_state import RestoreEntity | ||||
| from homeassistant.helpers.typing import ConfigType, GPSType, StateType | ||||
| from homeassistant.helpers.typing import ConfigType, GPSType | ||||
| from homeassistant.setup import ( | ||||
|     SetupPhases, | ||||
|     async_notify_setup_error, | ||||
| @@ -842,9 +842,11 @@ class Device(RestoreEntity): | ||||
|  | ||||
|     @final | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, StateType]: | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return the device state attributes.""" | ||||
|         attributes: dict[str, StateType] = {ATTR_SOURCE_TYPE: self.source_type} | ||||
|         attributes: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         attributes[ATTR_SOURCE_TYPE] = self.source_type | ||||
|  | ||||
|         if self.gps is not None: | ||||
|             attributes[ATTR_LATITUDE] = self.gps[0] | ||||
|   | ||||
| @@ -80,7 +80,8 @@ async def async_setup_entry( | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class DevoloScannerEntity( | ||||
| # The pylint disable is needed because of https://github.com/pylint-dev/pylint/issues/9138 | ||||
| class DevoloScannerEntity(  # pylint: disable=hass-enforce-class-module | ||||
|     CoordinatorEntity[DevoloDataUpdateCoordinator[dict[str, ConnectedStationInfo]]], | ||||
|     ScannerEntity, | ||||
| ): | ||||
|   | ||||
| @@ -122,12 +122,10 @@ class WanIpSensor(SensorEntity): | ||||
|         try: | ||||
|             async with asyncio.timeout(10): | ||||
|                 response = await self.resolver.query(self.hostname, self.querytype) | ||||
|         except TimeoutError as err: | ||||
|             _LOGGER.debug("Timeout while resolving host: %s", err) | ||||
|         except TimeoutError: | ||||
|             await self.resolver.close() | ||||
|         except DNSError as err: | ||||
|             _LOGGER.warning("Exception while resolving host: %s", err) | ||||
|             await self.resolver.close() | ||||
|  | ||||
|         if response: | ||||
|             sorted_ips = sort_ips( | ||||
|   | ||||
| @@ -6,6 +6,6 @@ | ||||
|   "documentation": "https://www.home-assistant.io/integrations/droplet", | ||||
|   "iot_class": "local_push", | ||||
|   "quality_scale": "bronze", | ||||
|   "requirements": ["pydroplet==2.3.4"], | ||||
|   "requirements": ["pydroplet==2.3.3"], | ||||
|   "zeroconf": ["_droplet._tcp.local."] | ||||
| } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||
|  | ||||
| from . import EcovacsConfigEntry | ||||
| from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS | ||||
| from .const import SUPPORTED_LIFESPANS | ||||
| from .entity import ( | ||||
|     EcovacsCapabilityEntityDescription, | ||||
|     EcovacsDescriptionEntity, | ||||
| @@ -62,7 +62,7 @@ STATION_ENTITY_DESCRIPTIONS = tuple( | ||||
|         key=f"station_action_{action.name.lower()}", | ||||
|         translation_key=f"station_action_{action.name.lower()}", | ||||
|     ) | ||||
|     for action in SUPPORTED_STATION_ACTIONS | ||||
|     for action in StationAction | ||||
| ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -23,11 +23,7 @@ SUPPORTED_LIFESPANS = ( | ||||
|     LifeSpan.STATION_FILTER, | ||||
| ) | ||||
|  | ||||
| SUPPORTED_STATION_ACTIONS = ( | ||||
|     StationAction.CLEAN_BASE, | ||||
|     StationAction.DRY_MOP, | ||||
|     StationAction.EMPTY_DUSTBIN, | ||||
| ) | ||||
| SUPPORTED_STATION_ACTIONS = (StationAction.EMPTY_DUSTBIN,) | ||||
|  | ||||
| LEGACY_SUPPORTED_LIFESPANS = ( | ||||
|     "main_brush", | ||||
|   | ||||
| @@ -36,12 +36,6 @@ | ||||
|       "reset_lifespan_round_mop": { | ||||
|         "default": "mdi:broom" | ||||
|       }, | ||||
|       "station_action_clean_base": { | ||||
|         "default": "mdi:home" | ||||
|       }, | ||||
|       "station_action_dry_mop": { | ||||
|         "default": "mdi:broom" | ||||
|       }, | ||||
|       "station_action_empty_dustbin": { | ||||
|         "default": "mdi:delete-restore" | ||||
|       } | ||||
|   | ||||
| @@ -4,8 +4,7 @@ | ||||
|   "codeowners": ["@mib1185", "@edenhaus", "@Augar"], | ||||
|   "config_flow": true, | ||||
|   "documentation": "https://www.home-assistant.io/integrations/ecovacs", | ||||
|   "integration_type": "hub", | ||||
|   "iot_class": "cloud_push", | ||||
|   "loggers": ["sleekxmppfs", "sucks", "deebot_client"], | ||||
|   "requirements": ["py-sucks==0.9.11", "deebot-client==16.1.0"] | ||||
|   "requirements": ["py-sucks==0.9.11", "deebot-client==15.1.0"] | ||||
| } | ||||
|   | ||||
| @@ -70,12 +70,6 @@ | ||||
|       "reset_lifespan_side_brush": { | ||||
|         "name": "Reset side brush lifespan" | ||||
|       }, | ||||
|       "station_action_clean_base": { | ||||
|         "name": "Clean base" | ||||
|       }, | ||||
|       "station_action_dry_mop": { | ||||
|         "name": "Dry mop" | ||||
|       }, | ||||
|       "station_action_empty_dustbin": { | ||||
|         "name": "Empty dustbin" | ||||
|       } | ||||
|   | ||||
| @@ -21,9 +21,6 @@ DEFAULT_STT_MODEL = "scribe_v1" | ||||
| DEFAULT_STYLE = 0 | ||||
| DEFAULT_USE_SPEAKER_BOOST = True | ||||
|  | ||||
| MAX_REQUEST_IDS = 3 | ||||
| MODELS_PREVIOUS_INFO_NOT_SUPPORTED = ("eleven_v3",) | ||||
|  | ||||
| STT_LANGUAGES = [ | ||||
|     "af-ZA",  # Afrikaans | ||||
|     "am-ET",  # Amharic | ||||
|   | ||||
| @@ -7,5 +7,5 @@ | ||||
|   "integration_type": "service", | ||||
|   "iot_class": "cloud_polling", | ||||
|   "loggers": ["elevenlabs"], | ||||
|   "requirements": ["elevenlabs==2.3.0", "sentence-stream==1.2.0"] | ||||
|   "requirements": ["elevenlabs==2.3.0"] | ||||
| } | ||||
|   | ||||
| @@ -85,4 +85,4 @@ rules: | ||||
|   # Platinum | ||||
|   async-dependency: done | ||||
|   inject-websession: done | ||||
|   strict-typing: todo | ||||
|   strict-typing: done | ||||
|   | ||||
| @@ -2,23 +2,17 @@ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| from collections import deque | ||||
| from collections.abc import AsyncGenerator, Mapping | ||||
| import contextlib | ||||
| from collections.abc import Mapping | ||||
| import logging | ||||
| from typing import Any | ||||
|  | ||||
| from elevenlabs import AsyncElevenLabs | ||||
| from elevenlabs.core import ApiError | ||||
| from elevenlabs.types import Model, Voice as ElevenLabsVoice, VoiceSettings | ||||
| from sentence_stream import SentenceBoundaryDetector | ||||
|  | ||||
| from homeassistant.components.tts import ( | ||||
|     ATTR_VOICE, | ||||
|     TextToSpeechEntity, | ||||
|     TTSAudioRequest, | ||||
|     TTSAudioResponse, | ||||
|     TtsAudioType, | ||||
|     Voice, | ||||
| ) | ||||
| @@ -41,12 +35,10 @@ from .const import ( | ||||
|     DEFAULT_STYLE, | ||||
|     DEFAULT_USE_SPEAKER_BOOST, | ||||
|     DOMAIN, | ||||
|     MAX_REQUEST_IDS, | ||||
|     MODELS_PREVIOUS_INFO_NOT_SUPPORTED, | ||||
| ) | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| PARALLEL_UPDATES = 6 | ||||
| PARALLEL_UPDATES = 0 | ||||
|  | ||||
|  | ||||
| def to_voice_settings(options: Mapping[str, Any]) -> VoiceSettings: | ||||
| @@ -130,12 +122,7 @@ class ElevenLabsTTSEntity(TextToSpeechEntity): | ||||
|         self._attr_supported_languages = [ | ||||
|             lang.language_id for lang in self._model.languages or [] | ||||
|         ] | ||||
|         # Use the first supported language as the default if available | ||||
|         self._attr_default_language = ( | ||||
|             self._attr_supported_languages[0] | ||||
|             if self._attr_supported_languages | ||||
|             else "en" | ||||
|         ) | ||||
|         self._attr_default_language = self.supported_languages[0] | ||||
|  | ||||
|     def async_get_supported_voices(self, language: str) -> list[Voice]: | ||||
|         """Return a list of supported voices for a language.""" | ||||
| @@ -164,151 +151,3 @@ class ElevenLabsTTSEntity(TextToSpeechEntity): | ||||
|             ) | ||||
|             raise HomeAssistantError(exc) from exc | ||||
|         return "mp3", bytes_combined | ||||
|  | ||||
|     async def async_stream_tts_audio( | ||||
|         self, request: TTSAudioRequest | ||||
|     ) -> TTSAudioResponse: | ||||
|         """Generate speech from an incoming message.""" | ||||
|         _LOGGER.debug( | ||||
|             "Getting TTS audio for language %s and options: %s", | ||||
|             request.language, | ||||
|             request.options, | ||||
|         ) | ||||
|         return TTSAudioResponse("mp3", self._process_tts_stream(request)) | ||||
|  | ||||
|     async def _process_tts_stream( | ||||
|         self, request: TTSAudioRequest | ||||
|     ) -> AsyncGenerator[bytes]: | ||||
|         """Generate speech from an incoming message.""" | ||||
|         text_stream = request.message_gen | ||||
|         boundary_detector = SentenceBoundaryDetector() | ||||
|         sentences: list[str] = [] | ||||
|         sentences_ready = asyncio.Event() | ||||
|         sentences_complete = False | ||||
|  | ||||
|         language_code: str | None = request.language | ||||
|         voice_id = request.options.get(ATTR_VOICE, self._default_voice_id) | ||||
|         model = request.options.get(ATTR_MODEL, self._model.model_id) | ||||
|  | ||||
|         use_request_ids = model not in MODELS_PREVIOUS_INFO_NOT_SUPPORTED | ||||
|         previous_request_ids: deque[str] = deque(maxlen=MAX_REQUEST_IDS) | ||||
|  | ||||
|         base_stream_params = { | ||||
|             "voice_id": voice_id, | ||||
|             "model_id": model, | ||||
|             "output_format": "mp3_44100_128", | ||||
|             "voice_settings": self._voice_settings, | ||||
|         } | ||||
|         if language_code: | ||||
|             base_stream_params["language_code"] = language_code | ||||
|  | ||||
|         _LOGGER.debug("Starting TTS Stream with options: %s", base_stream_params) | ||||
|  | ||||
|         async def _add_sentences() -> None: | ||||
|             nonlocal sentences_complete | ||||
|  | ||||
|             try: | ||||
|                 # Text chunks may not be on word or sentence boundaries | ||||
|                 async for text_chunk in text_stream: | ||||
|                     for sentence in boundary_detector.add_chunk(text_chunk): | ||||
|                         if not sentence.strip(): | ||||
|                             continue | ||||
|  | ||||
|                         sentences.append(sentence) | ||||
|  | ||||
|                     if not sentences: | ||||
|                         continue | ||||
|  | ||||
|                     sentences_ready.set() | ||||
|  | ||||
|                 # Final sentence | ||||
|                 if text := boundary_detector.finish(): | ||||
|                     sentences.append(text) | ||||
|             finally: | ||||
|                 sentences_complete = True | ||||
|                 sentences_ready.set() | ||||
|  | ||||
|         _add_sentences_task = self.hass.async_create_background_task( | ||||
|             _add_sentences(), name="elevenlabs_tts_add_sentences" | ||||
|         ) | ||||
|  | ||||
|         # Process new sentences as they're available, but synthesize the first | ||||
|         # one immediately. While that's playing, synthesize (up to) the next 3 | ||||
|         # sentences. After that, synthesize all completed sentences as they're | ||||
|         # available. | ||||
|         sentence_schedule = [1, 3] | ||||
|         while True: | ||||
|             await sentences_ready.wait() | ||||
|  | ||||
|             # Don't wait again if no more sentences are coming | ||||
|             if not sentences_complete: | ||||
|                 sentences_ready.clear() | ||||
|  | ||||
|             if not sentences: | ||||
|                 if sentences_complete: | ||||
|                     # Exit TTS loop | ||||
|                     _LOGGER.debug("No more sentences to process") | ||||
|                     break | ||||
|  | ||||
|                 # More sentences may be coming | ||||
|                 continue | ||||
|  | ||||
|             new_sentences = sentences[:] | ||||
|             sentences.clear() | ||||
|  | ||||
|             while new_sentences: | ||||
|                 if sentence_schedule: | ||||
|                     max_sentences = sentence_schedule.pop(0) | ||||
|                     sentences_to_process = new_sentences[:max_sentences] | ||||
|                     new_sentences = new_sentences[len(sentences_to_process) :] | ||||
|                 else: | ||||
|                     # Process all available sentences together | ||||
|                     sentences_to_process = new_sentences[:] | ||||
|                     new_sentences.clear() | ||||
|  | ||||
|                 # Combine all new sentences completed to this point | ||||
|                 text = " ".join(sentences_to_process).strip() | ||||
|  | ||||
|                 if not text: | ||||
|                     continue | ||||
|  | ||||
|                 # Build kwargs common to both modes | ||||
|                 kwargs = base_stream_params | { | ||||
|                     "text": text, | ||||
|                 } | ||||
|  | ||||
|                 # Provide previous_request_ids if supported. | ||||
|                 if previous_request_ids: | ||||
|                     # Send previous request ids. | ||||
|                     kwargs["previous_request_ids"] = list(previous_request_ids) | ||||
|  | ||||
|                 # Synthesize audio while text chunks are still being accumulated | ||||
|                 _LOGGER.debug("Synthesizing TTS for text: %s", text) | ||||
|                 try: | ||||
|                     async with self._client.text_to_speech.with_raw_response.stream( | ||||
|                         **kwargs | ||||
|                     ) as stream: | ||||
|                         async for chunk_bytes in stream.data: | ||||
|                             yield chunk_bytes | ||||
|  | ||||
|                         if use_request_ids: | ||||
|                             if (rid := stream.headers.get("request-id")) is not None: | ||||
|                                 previous_request_ids.append(rid) | ||||
|                             else: | ||||
|                                 _LOGGER.debug( | ||||
|                                     "No request-id returned from server; clearing previous requests" | ||||
|                                 ) | ||||
|                                 previous_request_ids.clear() | ||||
|                 except ApiError as exc: | ||||
|                     _LOGGER.warning( | ||||
|                         "Error during processing of TTS request %s", exc, exc_info=True | ||||
|                     ) | ||||
|                     _add_sentences_task.cancel() | ||||
|                     with contextlib.suppress(asyncio.CancelledError): | ||||
|                         await _add_sentences_task | ||||
|                     raise HomeAssistantError(exc) from exc | ||||
|  | ||||
|                 # Capture and store server request-id for next calls (only when supported) | ||||
|                 _LOGGER.debug("Completed TTS stream for text: %s", text) | ||||
|  | ||||
|         _LOGGER.debug("Completed TTS stream") | ||||
|   | ||||
| @@ -9,7 +9,6 @@ from typing import Any, cast | ||||
| from aioesphomeapi import ( | ||||
|     ClimateAction, | ||||
|     ClimateFanMode, | ||||
|     ClimateFeature, | ||||
|     ClimateInfo, | ||||
|     ClimateMode, | ||||
|     ClimatePreset, | ||||
| @@ -135,16 +134,12 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti | ||||
|  | ||||
|     _attr_temperature_unit = UnitOfTemperature.CELSIUS | ||||
|     _attr_translation_key = "climate" | ||||
|     _feature_flags = ClimateFeature(0) | ||||
|  | ||||
|     @callback | ||||
|     def _on_static_info_update(self, static_info: EntityInfo) -> None: | ||||
|         """Set attrs from static info.""" | ||||
|         super()._on_static_info_update(static_info) | ||||
|         static_info = self._static_info | ||||
|         self._feature_flags = ClimateFeature( | ||||
|             static_info.supported_feature_flags_compat(self._api_version) | ||||
|         ) | ||||
|         self._attr_precision = self._get_precision() | ||||
|         self._attr_hvac_modes = [ | ||||
|             _CLIMATE_MODES.from_esphome(mode) for mode in static_info.supported_modes | ||||
| @@ -168,18 +163,11 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti | ||||
|         self._attr_max_temp = static_info.visual_max_temperature | ||||
|         self._attr_min_humidity = round(static_info.visual_min_humidity) | ||||
|         self._attr_max_humidity = round(static_info.visual_max_humidity) | ||||
|         features = ClimateEntityFeature(0) | ||||
|         if self._feature_flags & ClimateFeature.SUPPORTS_TARGET_HUMIDITY: | ||||
|             features |= ClimateEntityFeature.TARGET_HUMIDITY | ||||
|         if self._feature_flags & ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE: | ||||
|         features = ClimateEntityFeature.TARGET_TEMPERATURE | ||||
|         if static_info.supports_two_point_target_temperature: | ||||
|             features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ||||
|         else: | ||||
|             features |= ClimateEntityFeature.TARGET_TEMPERATURE | ||||
|             if ( | ||||
|                 self._feature_flags | ||||
|                 & ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | ||||
|             ): | ||||
|                 features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ||||
|         if static_info.supports_target_humidity: | ||||
|             features |= ClimateEntityFeature.TARGET_HUMIDITY | ||||
|         if self.preset_modes: | ||||
|             features |= ClimateEntityFeature.PRESET_MODE | ||||
|         if self.fan_modes: | ||||
| @@ -215,7 +203,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti | ||||
|     def hvac_action(self) -> HVACAction | None: | ||||
|         """Return current action.""" | ||||
|         # HA has no support feature field for hvac_action | ||||
|         if not self._feature_flags & ClimateFeature.SUPPORTS_ACTION: | ||||
|         if not self._static_info.supports_action: | ||||
|             return None | ||||
|         return _CLIMATE_ACTIONS.from_esphome(self._state.action) | ||||
|  | ||||
| @@ -245,7 +233,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti | ||||
|     @esphome_float_state_property | ||||
|     def current_temperature(self) -> float | None: | ||||
|         """Return the current temperature.""" | ||||
|         if not self._feature_flags & ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE: | ||||
|         if not self._static_info.supports_current_temperature: | ||||
|             return None | ||||
|         return self._state.current_temperature | ||||
|  | ||||
| @@ -254,7 +242,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti | ||||
|     def current_humidity(self) -> int | None: | ||||
|         """Return the current humidity.""" | ||||
|         if ( | ||||
|             (not self._feature_flags & ClimateFeature.SUPPORTS_CURRENT_HUMIDITY) | ||||
|             not self._static_info.supports_current_humidity | ||||
|             or (val := self._state.current_humidity) is None | ||||
|             or not isfinite(val) | ||||
|         ): | ||||
| @@ -266,11 +254,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti | ||||
|     def target_temperature(self) -> float | None: | ||||
|         """Return the temperature we try to reach.""" | ||||
|         if ( | ||||
|             not self._feature_flags | ||||
|             & ( | ||||
|                 ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE | ||||
|                 | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | ||||
|             ) | ||||
|             not self._static_info.supports_two_point_target_temperature | ||||
|             and self.hvac_mode != HVACMode.AUTO | ||||
|         ): | ||||
|             return self._state.target_temperature | ||||
| @@ -311,10 +295,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti | ||||
|                 cast(HVACMode, kwargs[ATTR_HVAC_MODE]) | ||||
|             ) | ||||
|         if ATTR_TEMPERATURE in kwargs: | ||||
|             if not self._feature_flags & ( | ||||
|                 ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE | ||||
|                 | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | ||||
|             ): | ||||
|             if not self._static_info.supports_two_point_target_temperature: | ||||
|                 data["target_temperature"] = kwargs[ATTR_TEMPERATURE] | ||||
|             else: | ||||
|                 hvac_mode = kwargs.get(ATTR_HVAC_MODE) or self.hvac_mode | ||||
|   | ||||
| @@ -542,16 +542,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|  | ||||
|         # Check if Z-Wave capabilities are present and start discovery flow | ||||
|         next_flow_id: str | None = None | ||||
|         # If the zwave_home_id is not set, we don't know if it's a fresh | ||||
|         # adapter, or the cable is just unplugged. So only start | ||||
|         # the zwave_js config flow automatically if there is a | ||||
|         # zwave_home_id present. If it's a fresh adapter, the manager | ||||
|         # will handle starting the flow once it gets the home id changed | ||||
|         # request from the ESPHome device. | ||||
|         if ( | ||||
|             self._device_info.zwave_proxy_feature_flags | ||||
|             and self._device_info.zwave_home_id | ||||
|         ): | ||||
|         if self._device_info.zwave_proxy_feature_flags: | ||||
|             assert self._connected_address is not None | ||||
|             assert self._port is not None | ||||
|  | ||||
| @@ -568,7 +559,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|                 }, | ||||
|                 data=ESPHomeServiceInfo( | ||||
|                     name=self._device_info.name, | ||||
|                     zwave_home_id=self._device_info.zwave_home_id, | ||||
|                     zwave_home_id=self._device_info.zwave_home_id or None, | ||||
|                     ip_address=self._connected_address, | ||||
|                     port=self._port, | ||||
|                     noise_psk=self._noise_psk, | ||||
|   | ||||
| @@ -491,30 +491,13 @@ class RuntimeEntryData: | ||||
|  | ||||
|         assert self.client.connected_address | ||||
|  | ||||
|         # If the device does not have a zwave_home_id, it means | ||||
|         # either the Z-Wave controller has never been connected | ||||
|         # to the ESPHome device, or the Z-Wave controller has | ||||
|         # never been provisioned with a home ID (brand new). | ||||
|         # Since we cannot tell the difference, and it could | ||||
|         # just be the cable is unplugged we only | ||||
|         # automatically start the flow if we have a home ID. | ||||
|         if not device_info.zwave_home_id: | ||||
|             return | ||||
|  | ||||
|         self.async_create_zwave_js_flow(hass, device_info, device_info.zwave_home_id) | ||||
|  | ||||
|     def async_create_zwave_js_flow( | ||||
|         self, hass: HomeAssistant, device_info: DeviceInfo, zwave_home_id: int | ||||
|     ) -> None: | ||||
|         """Create a zwave_js config flow for a Z-Wave JS Proxy device.""" | ||||
|         assert self.client.connected_address is not None | ||||
|         discovery_flow.async_create_flow( | ||||
|             hass, | ||||
|             "zwave_js", | ||||
|             {"source": config_entries.SOURCE_ESPHOME}, | ||||
|             ESPHomeServiceInfo( | ||||
|                 name=device_info.name, | ||||
|                 zwave_home_id=zwave_home_id, | ||||
|                 zwave_home_id=device_info.zwave_home_id or None, | ||||
|                 ip_address=self.client.connected_address, | ||||
|                 port=self.client.port, | ||||
|                 noise_psk=self.client.noise_psk, | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import base64 | ||||
| from functools import partial | ||||
| import logging | ||||
| import secrets | ||||
| import struct | ||||
| from typing import TYPE_CHECKING, Any, NamedTuple | ||||
|  | ||||
| from aioesphomeapi import ( | ||||
| @@ -23,8 +22,6 @@ from aioesphomeapi import ( | ||||
|     RequiresEncryptionAPIError, | ||||
|     UserService, | ||||
|     UserServiceArgType, | ||||
|     ZWaveProxyRequest, | ||||
|     ZWaveProxyRequestType, | ||||
|     parse_log_message, | ||||
| ) | ||||
| from awesomeversion import AwesomeVersion | ||||
| @@ -47,18 +44,12 @@ from homeassistant.core import ( | ||||
|     State, | ||||
|     callback, | ||||
| ) | ||||
| from homeassistant.exceptions import ( | ||||
|     HomeAssistantError, | ||||
|     ServiceNotFound, | ||||
|     ServiceValidationError, | ||||
|     TemplateError, | ||||
| ) | ||||
| from homeassistant.exceptions import HomeAssistantError, TemplateError | ||||
| from homeassistant.helpers import ( | ||||
|     config_validation as cv, | ||||
|     device_registry as dr, | ||||
|     entity_registry as er, | ||||
|     issue_registry as ir, | ||||
|     json, | ||||
|     template, | ||||
| ) | ||||
| from homeassistant.helpers.device_registry import format_mac | ||||
| @@ -93,8 +84,6 @@ from .encryption_key_storage import async_get_encryption_key_storage | ||||
| from .entry_data import ESPHomeConfigEntry, RuntimeEntryData | ||||
|  | ||||
| DEVICE_CONFLICT_ISSUE_FORMAT = "device_conflict-{}" | ||||
| UNPACK_UINT32_BE = struct.Struct(">I").unpack_from | ||||
|  | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from aioesphomeapi.api_pb2 import SubscribeLogsResponse  # type: ignore[attr-defined]  # noqa: I001 | ||||
| @@ -279,32 +268,11 @@ class ESPHomeManager: | ||||
|         elif self.entry.options.get( | ||||
|             CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS | ||||
|         ): | ||||
|             call_id = service.call_id | ||||
|             if call_id and service.wants_response: | ||||
|                 # Service call with response expected | ||||
|                 self.entry.async_create_task( | ||||
|                     hass, | ||||
|                     self._handle_service_call_with_response( | ||||
|                         domain, | ||||
|                         service_name, | ||||
|                         service_data, | ||||
|                         call_id, | ||||
|                         service.response_template, | ||||
|                     ), | ||||
|                 ) | ||||
|             elif call_id: | ||||
|                 # Service call without response but needs success/failure notification | ||||
|                 self.entry.async_create_task( | ||||
|                     hass, | ||||
|                     self._handle_service_call_with_notification( | ||||
|                         domain, service_name, service_data, call_id | ||||
|                     ), | ||||
|                 ) | ||||
|             else: | ||||
|                 # Fire and forget service call | ||||
|                 self.entry.async_create_task( | ||||
|                     hass, hass.services.async_call(domain, service_name, service_data) | ||||
|             hass.async_create_task( | ||||
|                 hass.services.async_call( | ||||
|                     domain, service_name, service_data, blocking=True | ||||
|                 ) | ||||
|             ) | ||||
|         else: | ||||
|             device_info = self.entry_data.device_info | ||||
|             assert device_info is not None | ||||
| @@ -330,98 +298,6 @@ class ESPHomeManager: | ||||
|                 service_data, | ||||
|             ) | ||||
|  | ||||
|     async def _handle_service_call_with_response( | ||||
|         self, | ||||
|         domain: str, | ||||
|         service_name: str, | ||||
|         service_data: dict, | ||||
|         call_id: int, | ||||
|         response_template: str | None = None, | ||||
|     ) -> None: | ||||
|         """Handle service call that expects a response and send response back to ESPHome.""" | ||||
|         try: | ||||
|             # Call the service with response capture enabled | ||||
|             action_response = await self.hass.services.async_call( | ||||
|                 domain=domain, | ||||
|                 service=service_name, | ||||
|                 service_data=service_data, | ||||
|                 blocking=True, | ||||
|                 return_response=True, | ||||
|             ) | ||||
|  | ||||
|             if response_template: | ||||
|                 try: | ||||
|                     # Render response template | ||||
|                     tmpl = Template(response_template, self.hass) | ||||
|                     response = tmpl.async_render( | ||||
|                         variables={"response": action_response}, | ||||
|                         strict=True, | ||||
|                     ) | ||||
|                     response_dict = {"response": response} | ||||
|  | ||||
|                 except TemplateError as ex: | ||||
|                     raise HomeAssistantError( | ||||
|                         f"Error rendering response template: {ex}" | ||||
|                     ) from ex | ||||
|             else: | ||||
|                 response_dict = {"response": action_response} | ||||
|  | ||||
|             # JSON encode response data for ESPHome | ||||
|             response_data = json.json_bytes(response_dict) | ||||
|  | ||||
|         except ( | ||||
|             ServiceNotFound, | ||||
|             ServiceValidationError, | ||||
|             vol.Invalid, | ||||
|             HomeAssistantError, | ||||
|         ) as ex: | ||||
|             self._send_service_call_response( | ||||
|                 call_id, success=False, error_message=str(ex), response_data=b"" | ||||
|             ) | ||||
|  | ||||
|         else: | ||||
|             # Send success response back to ESPHome | ||||
|             self._send_service_call_response( | ||||
|                 call_id=call_id, | ||||
|                 success=True, | ||||
|                 error_message="", | ||||
|                 response_data=response_data, | ||||
|             ) | ||||
|  | ||||
|     async def _handle_service_call_with_notification( | ||||
|         self, domain: str, service_name: str, service_data: dict, call_id: int | ||||
|     ) -> None: | ||||
|         """Handle service call that needs success/failure notification.""" | ||||
|         try: | ||||
|             await self.hass.services.async_call( | ||||
|                 domain, service_name, service_data, blocking=True | ||||
|             ) | ||||
|         except (ServiceNotFound, ServiceValidationError, vol.Invalid) as ex: | ||||
|             self._send_service_call_response(call_id, False, str(ex), b"") | ||||
|         else: | ||||
|             self._send_service_call_response(call_id, True, "", b"") | ||||
|  | ||||
|     def _send_service_call_response( | ||||
|         self, | ||||
|         call_id: int, | ||||
|         success: bool, | ||||
|         error_message: str, | ||||
|         response_data: bytes, | ||||
|     ) -> None: | ||||
|         """Send service call response back to ESPHome device.""" | ||||
|         _LOGGER.debug( | ||||
|             "Service call response for call_id %s: success=%s, error=%s", | ||||
|             call_id, | ||||
|             success, | ||||
|             error_message, | ||||
|         ) | ||||
|         self.cli.send_homeassistant_action_response( | ||||
|             call_id, | ||||
|             success, | ||||
|             error_message, | ||||
|             response_data, | ||||
|         ) | ||||
|  | ||||
|     @callback | ||||
|     def _send_home_assistant_state( | ||||
|         self, entity_id: str, attribute: str | None, state: State | None | ||||
| @@ -681,11 +557,6 @@ class ESPHomeManager: | ||||
|             ) | ||||
|             entry_data.loaded_platforms.add(Platform.ASSIST_SATELLITE) | ||||
|  | ||||
|         if device_info.zwave_proxy_feature_flags: | ||||
|             entry_data.disconnect_callbacks.add( | ||||
|                 cli.subscribe_zwave_proxy_request(self._async_zwave_proxy_request) | ||||
|             ) | ||||
|  | ||||
|         cli.subscribe_home_assistant_states_and_services( | ||||
|             on_state=entry_data.async_update_state, | ||||
|             on_service_call=self.async_on_service_call, | ||||
| @@ -697,25 +568,6 @@ class ESPHomeManager: | ||||
|         _async_check_firmware_version(hass, device_info, api_version) | ||||
|         _async_check_using_api_password(hass, device_info, bool(self.password)) | ||||
|  | ||||
|     def _async_zwave_proxy_request(self, request: ZWaveProxyRequest) -> None: | ||||
|         """Handle a request to create a zwave_js config flow.""" | ||||
|         if request.type != ZWaveProxyRequestType.HOME_ID_CHANGE: | ||||
|             return | ||||
|         # ESPHome will send a home id change on every connection | ||||
|         # if the Z-Wave controller is connected to the ESPHome device | ||||
|         # so we know for sure that the Z-Wave controller is connected | ||||
|         # when we get the message. This makes it safe to start | ||||
|         # the zwave_js config flow automatically even if the zwave_home_id | ||||
|         # is 0 (not yet provisioned) as we know for sure the controller | ||||
|         # is connected to the ESPHome device and do not have to guess | ||||
|         # if it's a broken connection or Z-Wave controller or a not | ||||
|         # yet provisioned controller. | ||||
|         zwave_home_id: int = UNPACK_UINT32_BE(request.data[0:4])[0] | ||||
|         assert self.entry_data.device_info is not None | ||||
|         self.entry_data.async_create_zwave_js_flow( | ||||
|             self.hass, self.entry_data.device_info, zwave_home_id | ||||
|         ) | ||||
|  | ||||
|     async def on_disconnect(self, expected_disconnect: bool) -> None: | ||||
|         """Run disconnect callbacks on API disconnect.""" | ||||
|         entry_data = self.entry_data | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
|   "mqtt": ["esphome/discover/#"], | ||||
|   "quality_scale": "platinum", | ||||
|   "requirements": [ | ||||
|     "aioesphomeapi==42.2.0", | ||||
|     "aioesphomeapi==42.0.0", | ||||
|     "esphome-dashboard-api==1.3.0", | ||||
|     "bleak-esphome==3.4.0" | ||||
|   ], | ||||
|   | ||||
| @@ -180,7 +180,9 @@ class EventEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return the state attributes.""" | ||||
|         attributes = {ATTR_EVENT_TYPE: self.__last_event_type} | ||||
|         attributes: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         attributes[ATTR_EVENT_TYPE] = self.__last_event_type | ||||
|         if last_event_attributes := self.__last_event_attributes: | ||||
|             attributes |= last_event_attributes | ||||
|         return attributes | ||||
|   | ||||
| @@ -385,9 +385,10 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): | ||||
|  | ||||
|     @final | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, float | str | None]: | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return optional state attributes.""" | ||||
|         data: dict[str, float | str | None] = {} | ||||
|         data: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         supported_features = self.supported_features | ||||
|  | ||||
|         if FanEntityFeature.DIRECTION in supported_features: | ||||
|   | ||||
| @@ -4,7 +4,6 @@ | ||||
|   "codeowners": ["@mib1185"], | ||||
|   "config_flow": true, | ||||
|   "documentation": "https://www.home-assistant.io/integrations/feedreader", | ||||
|   "integration_type": "service", | ||||
|   "iot_class": "cloud_polling", | ||||
|   "loggers": ["feedparser", "sgmllib3k"], | ||||
|   "requirements": ["feedparser==6.0.12"] | ||||
|   | ||||
| @@ -1,42 +0,0 @@ | ||||
| """The Fing integration.""" | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import logging | ||||
|  | ||||
| from homeassistant.const import Platform | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.exceptions import ConfigEntryError | ||||
|  | ||||
| from .coordinator import FingConfigEntry, FingDataUpdateCoordinator | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| PLATFORMS = [Platform.DEVICE_TRACKER] | ||||
|  | ||||
|  | ||||
| async def async_setup_entry(hass: HomeAssistant, config_entry: FingConfigEntry) -> bool: | ||||
|     """Set up the Fing component.""" | ||||
|  | ||||
|     coordinator = FingDataUpdateCoordinator(hass, config_entry) | ||||
|     await coordinator.async_config_entry_first_refresh() | ||||
|  | ||||
|     if coordinator.data.network_id is None: | ||||
|         _LOGGER.warning( | ||||
|             "Skip setting up Fing integration; Received an empty NetworkId from the request - Check if the API version is the latest" | ||||
|         ) | ||||
|         raise ConfigEntryError( | ||||
|             "The Agent's API version is outdated. Please update the agent to the latest version." | ||||
|         ) | ||||
|  | ||||
|     config_entry.runtime_data = coordinator | ||||
|     await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) | ||||
|  | ||||
|     return True | ||||
|  | ||||
|  | ||||
| async def async_unload_entry( | ||||
|     hass: HomeAssistant, config_entry: FingConfigEntry | ||||
| ) -> bool: | ||||
|     """Unload a config entry.""" | ||||
|     return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) | ||||
| @@ -1,114 +0,0 @@ | ||||
| """Config flow file.""" | ||||
|  | ||||
| from contextlib import suppress | ||||
| import logging | ||||
| from typing import Any | ||||
|  | ||||
| from fing_agent_api import FingAgent | ||||
| import httpx | ||||
| import voluptuous as vol | ||||
|  | ||||
| from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||||
| from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT | ||||
|  | ||||
| from .const import DOMAIN, UPNP_AVAILABLE | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class FingConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|     """Fing config flow.""" | ||||
|  | ||||
|     VERSION = 1 | ||||
|  | ||||
|     async def async_step_user( | ||||
|         self, user_input: dict[str, Any] | None = None | ||||
|     ) -> ConfigFlowResult: | ||||
|         """Set up user step.""" | ||||
|         errors: dict[str, str] = {} | ||||
|         description_placeholders: dict[str, str] = {} | ||||
|  | ||||
|         if user_input is not None: | ||||
|             devices_response = None | ||||
|             agent_info_response = None | ||||
|  | ||||
|             self._async_abort_entries_match( | ||||
|                 {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]} | ||||
|             ) | ||||
|  | ||||
|             fing_api = FingAgent( | ||||
|                 ip=user_input[CONF_IP_ADDRESS], | ||||
|                 port=int(user_input[CONF_PORT]), | ||||
|                 key=user_input[CONF_API_KEY], | ||||
|             ) | ||||
|  | ||||
|             try: | ||||
|                 devices_response = await fing_api.get_devices() | ||||
|  | ||||
|                 with suppress(httpx.ConnectError): | ||||
|                     # The suppression is needed because the get_agent_info method isn't available for desktop agents | ||||
|                     agent_info_response = await fing_api.get_agent_info() | ||||
|  | ||||
|             except httpx.NetworkError as _: | ||||
|                 errors["base"] = "cannot_connect" | ||||
|             except httpx.TimeoutException as _: | ||||
|                 errors["base"] = "timeout_connect" | ||||
|             except httpx.HTTPStatusError as exception: | ||||
|                 description_placeholders["message"] = ( | ||||
|                     f"{exception.response.status_code} - {exception.response.reason_phrase}" | ||||
|                 ) | ||||
|                 if exception.response.status_code == 401: | ||||
|                     errors["base"] = "invalid_api_key" | ||||
|                 else: | ||||
|                     errors["base"] = "http_status_error" | ||||
|             except httpx.InvalidURL as _: | ||||
|                 errors["base"] = "url_error" | ||||
|             except ( | ||||
|                 httpx.HTTPError, | ||||
|                 httpx.CookieConflict, | ||||
|                 httpx.StreamError, | ||||
|             ) as ex: | ||||
|                 _LOGGER.error("Unexpected exception: %s", ex) | ||||
|                 errors["base"] = "unknown" | ||||
|             else: | ||||
|                 if ( | ||||
|                     devices_response.network_id is not None | ||||
|                     and len(devices_response.network_id) > 0 | ||||
|                 ): | ||||
|                     agent_name = user_input.get(CONF_IP_ADDRESS) | ||||
|                     upnp_available = False | ||||
|                     if agent_info_response is not None: | ||||
|                         upnp_available = True | ||||
|                         agent_name = agent_info_response.agent_id | ||||
|                         await self.async_set_unique_id(agent_info_response.agent_id) | ||||
|                         self._abort_if_unique_id_configured() | ||||
|  | ||||
|                     data = { | ||||
|                         CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], | ||||
|                         CONF_PORT: user_input[CONF_PORT], | ||||
|                         CONF_API_KEY: user_input[CONF_API_KEY], | ||||
|                         UPNP_AVAILABLE: upnp_available, | ||||
|                     } | ||||
|  | ||||
|                     return self.async_create_entry( | ||||
|                         title=f"Fing Agent {agent_name}", | ||||
|                         data=data, | ||||
|                     ) | ||||
|  | ||||
|                 return self.async_abort(reason="api_version_error") | ||||
|  | ||||
|         return self.async_show_form( | ||||
|             step_id="user", | ||||
|             data_schema=self.add_suggested_values_to_schema( | ||||
|                 vol.Schema( | ||||
|                     { | ||||
|                         vol.Required(CONF_IP_ADDRESS): str, | ||||
|                         vol.Required(CONF_PORT, default="49090"): str, | ||||
|                         vol.Required(CONF_API_KEY): str, | ||||
|                     } | ||||
|                 ), | ||||
|                 user_input, | ||||
|             ), | ||||
|             errors=errors, | ||||
|             description_placeholders=description_placeholders, | ||||
|         ) | ||||
| @@ -1,4 +0,0 @@ | ||||
| """Const for the Fing integration.""" | ||||
|  | ||||
| DOMAIN = "fing" | ||||
| UPNP_AVAILABLE = "upnp_available" | ||||
| @@ -1,85 +0,0 @@ | ||||
| """DataUpdateCoordinator for Fing integration.""" | ||||
|  | ||||
| from dataclasses import dataclass, field | ||||
| from datetime import timedelta | ||||
| import logging | ||||
|  | ||||
| from fing_agent_api import FingAgent | ||||
| from fing_agent_api.models import AgentInfoResponse, Device | ||||
| import httpx | ||||
|  | ||||
| from homeassistant.config_entries import ConfigEntry | ||||
| from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||||
|  | ||||
| from .const import DOMAIN, UPNP_AVAILABLE | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| type FingConfigEntry = ConfigEntry[FingDataUpdateCoordinator] | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class FingDataObject: | ||||
|     """Fing Data Object.""" | ||||
|  | ||||
|     network_id: str | None = None | ||||
|     agent_info: AgentInfoResponse | None = None | ||||
|     devices: dict[str, Device] = field(default_factory=dict) | ||||
|  | ||||
|  | ||||
| class FingDataUpdateCoordinator(DataUpdateCoordinator[FingDataObject]): | ||||
|     """Class to manage fetching data from Fing Agent.""" | ||||
|  | ||||
|     def __init__(self, hass: HomeAssistant, config_entry: FingConfigEntry) -> None: | ||||
|         """Initialize global Fing updater.""" | ||||
|         self._fing = FingAgent( | ||||
|             ip=config_entry.data[CONF_IP_ADDRESS], | ||||
|             port=int(config_entry.data[CONF_PORT]), | ||||
|             key=config_entry.data[CONF_API_KEY], | ||||
|         ) | ||||
|         self._upnp_available = config_entry.data[UPNP_AVAILABLE] | ||||
|         update_interval = timedelta(seconds=30) | ||||
|         super().__init__( | ||||
|             hass, | ||||
|             _LOGGER, | ||||
|             name=DOMAIN, | ||||
|             update_interval=update_interval, | ||||
|             config_entry=config_entry, | ||||
|         ) | ||||
|  | ||||
|     async def _async_update_data(self) -> FingDataObject: | ||||
|         """Fetch data from Fing Agent.""" | ||||
|         device_response = None | ||||
|         agent_info_response = None | ||||
|         try: | ||||
|             device_response = await self._fing.get_devices() | ||||
|  | ||||
|             if self._upnp_available: | ||||
|                 agent_info_response = await self._fing.get_agent_info() | ||||
|  | ||||
|         except httpx.NetworkError as err: | ||||
|             raise UpdateFailed("Failed to connect") from err | ||||
|         except httpx.TimeoutException as err: | ||||
|             raise UpdateFailed("Timeout establishing connection") from err | ||||
|         except httpx.HTTPStatusError as err: | ||||
|             if err.response.status_code == 401: | ||||
|                 raise UpdateFailed("Invalid API key") from err | ||||
|             raise UpdateFailed( | ||||
|                 f"Http request failed -> {err.response.status_code} - {err.response.reason_phrase}" | ||||
|             ) from err | ||||
|         except httpx.InvalidURL as err: | ||||
|             raise UpdateFailed("Invalid hostname or IP address") from err | ||||
|         except ( | ||||
|             httpx.HTTPError, | ||||
|             httpx.CookieConflict, | ||||
|             httpx.StreamError, | ||||
|         ) as err: | ||||
|             raise UpdateFailed("Unexpected error from HTTP request") from err | ||||
|         else: | ||||
|             return FingDataObject( | ||||
|                 device_response.network_id, | ||||
|                 agent_info_response, | ||||
|                 {device.mac: device for device in device_response.devices}, | ||||
|             ) | ||||
| @@ -1,127 +0,0 @@ | ||||
| """Platform for Device tracker integration.""" | ||||
|  | ||||
| from fing_agent_api.models import Device | ||||
|  | ||||
| from homeassistant.components.device_tracker import ScannerEntity | ||||
| from homeassistant.core import HomeAssistant, callback | ||||
| from homeassistant.helpers import entity_registry as er | ||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||
| from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||||
|  | ||||
| from . import FingConfigEntry | ||||
| from .coordinator import FingDataUpdateCoordinator | ||||
| from .utils import get_icon_from_type | ||||
|  | ||||
|  | ||||
| async def async_setup_entry( | ||||
|     hass: HomeAssistant, | ||||
|     config_entry: FingConfigEntry, | ||||
|     async_add_entities: AddConfigEntryEntitiesCallback, | ||||
| ) -> None: | ||||
|     """Add sensors for passed config_entry in HA.""" | ||||
|     coordinator = config_entry.runtime_data | ||||
|     entity_registry = er.async_get(hass) | ||||
|     tracked_devices: set[str] = set() | ||||
|  | ||||
|     @callback | ||||
|     def add_entities() -> None: | ||||
|         latest_devices = set(coordinator.data.devices.keys()) | ||||
|  | ||||
|         devices_to_remove = tracked_devices - set(latest_devices) | ||||
|         devices_to_add = set(latest_devices) - tracked_devices | ||||
|  | ||||
|         entities_to_remove = [] | ||||
|         for entity_entry in entity_registry.entities.values(): | ||||
|             if entity_entry.config_entry_id != config_entry.entry_id: | ||||
|                 continue | ||||
|             try: | ||||
|                 _, mac = entity_entry.unique_id.rsplit("-", 1) | ||||
|                 if mac in devices_to_remove: | ||||
|                     entities_to_remove.append(entity_entry.entity_id) | ||||
|             except ValueError: | ||||
|                 continue | ||||
|  | ||||
|         for entity_id in entities_to_remove: | ||||
|             entity_registry.async_remove(entity_id) | ||||
|  | ||||
|         entities_to_add = [] | ||||
|         for mac_addr in devices_to_add: | ||||
|             device = coordinator.data.devices[mac_addr] | ||||
|             entities_to_add.append(FingTrackedDevice(coordinator, device)) | ||||
|  | ||||
|         tracked_devices.clear() | ||||
|         tracked_devices.update(latest_devices) | ||||
|         async_add_entities(entities_to_add) | ||||
|  | ||||
|     add_entities() | ||||
|     config_entry.async_on_unload(coordinator.async_add_listener(add_entities)) | ||||
|  | ||||
|  | ||||
| class FingTrackedDevice(CoordinatorEntity[FingDataUpdateCoordinator], ScannerEntity): | ||||
|     """Represent a tracked device.""" | ||||
|  | ||||
|     _attr_has_entity_name = True | ||||
|  | ||||
|     def __init__(self, coordinator: FingDataUpdateCoordinator, device: Device) -> None: | ||||
|         """Set up FingDevice entity.""" | ||||
|         super().__init__(coordinator) | ||||
|  | ||||
|         self._device = device | ||||
|         agent_id = coordinator.data.network_id | ||||
|         if coordinator.data.agent_info is not None: | ||||
|             agent_id = coordinator.data.agent_info.agent_id | ||||
|  | ||||
|         self._attr_mac_address = self._device.mac | ||||
|         self._attr_unique_id = f"{agent_id}-{self._attr_mac_address}" | ||||
|         self._attr_name = self._device.name | ||||
|         self._attr_icon = get_icon_from_type(self._device.type) | ||||
|  | ||||
|     @property | ||||
|     def is_connected(self) -> bool: | ||||
|         """Return true if the device is connected to the network.""" | ||||
|         return self._device.active | ||||
|  | ||||
|     @property | ||||
|     def ip_address(self) -> str | None: | ||||
|         """Return the primary ip address of the device.""" | ||||
|         return self._device.ip[0] if self._device.ip else None | ||||
|  | ||||
|     @property | ||||
|     def entity_registry_enabled_default(self) -> bool: | ||||
|         """Enable entity by default.""" | ||||
|         return True | ||||
|  | ||||
|     @property | ||||
|     def unique_id(self) -> str | None: | ||||
|         """Return the unique ID of the entity.""" | ||||
|         return self._attr_unique_id | ||||
|  | ||||
|     def check_for_updates(self, new_device: Device) -> bool: | ||||
|         """Return true if the device has updates.""" | ||||
|         new_device_ip = new_device.ip[0] if new_device.ip else None | ||||
|         current_device_ip = self._device.ip[0] if self._device.ip else None | ||||
|  | ||||
|         return ( | ||||
|             current_device_ip != new_device_ip | ||||
|             or self._device.active != new_device.active | ||||
|             or self._device.type != new_device.type | ||||
|             or self._attr_name != new_device.name | ||||
|             or self._attr_icon != get_icon_from_type(new_device.type) | ||||
|         ) | ||||
|  | ||||
|     @callback | ||||
|     def _handle_coordinator_update(self) -> None: | ||||
|         """Handle updated data from the coordinator.""" | ||||
|         updated_device_data = self.coordinator.data.devices.get(self._device.mac) | ||||
|         if updated_device_data is not None and self.check_for_updates( | ||||
|             updated_device_data | ||||
|         ): | ||||
|             self._device = updated_device_data | ||||
|             self._attr_name = updated_device_data.name | ||||
|             self._attr_icon = get_icon_from_type(updated_device_data.type) | ||||
|             er.async_get(self.hass).async_update_entity( | ||||
|                 entity_id=self.entity_id, | ||||
|                 original_name=self._attr_name, | ||||
|                 original_icon=self._attr_icon, | ||||
|             ) | ||||
|             self.async_write_ha_state() | ||||
| @@ -1,10 +0,0 @@ | ||||
| { | ||||
|   "domain": "fing", | ||||
|   "name": "Fing", | ||||
|   "codeowners": ["@Lorenzo-Gasparini"], | ||||
|   "config_flow": true, | ||||
|   "documentation": "https://www.home-assistant.io/integrations/fing", | ||||
|   "iot_class": "local_polling", | ||||
|   "quality_scale": "bronze", | ||||
|   "requirements": ["fing_agent_api==1.0.3"] | ||||
| } | ||||
| @@ -1,72 +0,0 @@ | ||||
| rules: | ||||
|   # Bronze | ||||
|   action-setup: | ||||
|     status: exempt | ||||
|     comment: The integration has no actions. | ||||
|   appropriate-polling: done | ||||
|   brands: done | ||||
|   common-modules: done | ||||
|   config-flow: done | ||||
|   config-flow-test-coverage: done | ||||
|   dependency-transparency: done | ||||
|   docs-actions: | ||||
|     status: exempt | ||||
|     comment: There are no actions in Fing integration. | ||||
|   docs-high-level-description: done | ||||
|   docs-installation-instructions: done | ||||
|   docs-removal-instructions: done | ||||
|   entity-event-setup: | ||||
|     status: exempt | ||||
|     comment: Fing integration entities do not use events. | ||||
|   entity-unique-id: done | ||||
|   has-entity-name: done | ||||
|   runtime-data: done | ||||
|   test-before-configure: done | ||||
|   test-before-setup: done | ||||
|   unique-config-entry: done | ||||
|  | ||||
|   # Silver | ||||
|   action-exceptions: | ||||
|     status: exempt | ||||
|     comment: The integration has no actions. | ||||
|   config-entry-unloading: done | ||||
|   docs-configuration-parameters: | ||||
|     status: exempt | ||||
|     comment: The integration has no options flow. | ||||
|   docs-installation-parameters: done | ||||
|   entity-unavailable: done | ||||
|   integration-owner: done | ||||
|   log-when-unavailable: done | ||||
|   parallel-updates: todo | ||||
|   reauthentication-flow: todo | ||||
|   test-coverage: todo | ||||
|  | ||||
|   # Gold | ||||
|   devices: done | ||||
|   diagnostics: todo | ||||
|   discovery: todo | ||||
|   discovery-update-info: todo | ||||
|   docs-data-update: done | ||||
|   docs-examples: todo | ||||
|   docs-known-limitations: todo | ||||
|   docs-supported-devices: done | ||||
|   docs-supported-functions: done | ||||
|   docs-troubleshooting: done | ||||
|   docs-use-cases: todo | ||||
|   dynamic-devices: done | ||||
|   entity-category: todo | ||||
|   entity-device-class: | ||||
|     status: exempt | ||||
|     comment: The integration creates only device tracker entities | ||||
|   entity-disabled-by-default: todo | ||||
|   entity-translations: todo | ||||
|   exception-translations: todo | ||||
|   icon-translations: todo | ||||
|   reconfiguration-flow: todo | ||||
|   repair-issues: todo | ||||
|   stale-devices: done | ||||
|  | ||||
|   # Platinum | ||||
|   async-dependency: todo | ||||
|   inject-websession: todo | ||||
|   strict-typing: todo | ||||
| @@ -1,31 +0,0 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "title": "Set up Fing agent", | ||||
|         "data": { | ||||
|           "ip_address": "[%key:common::config_flow::data::ip%]", | ||||
|           "port": "[%key:common::config_flow::data::port%]", | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "ip_address": "IP address of the Fing agent.", | ||||
|           "port": "Port number of the Fing API.", | ||||
|           "api_key": "API key used to authenticate with the Fing API." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", | ||||
|       "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]", | ||||
|       "url_error": "[%key:common::config_flow::error::invalid_host%]", | ||||
|       "http_status_error": "HTTP request failed: {message}" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "api_version_error": "Your agent is using an outdated API version. The required 'network_id' parameter is missing. Please update to the latest API version." | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,85 +0,0 @@ | ||||
| """Utils functions.""" | ||||
|  | ||||
| from enum import Enum | ||||
|  | ||||
|  | ||||
| class DeviceType(Enum): | ||||
|     """Device types enum.""" | ||||
|  | ||||
|     GENERIC = "mdi:lan-connect" | ||||
|     MOBILE = PHONE = "mdi:cellphone" | ||||
|     TABLET = IPOD = EREADER = "mdi:tablet" | ||||
|     WATCH = WEARABLE = "mdi:watch" | ||||
|     CAR = AUTOMOTIVE = "mdi:car-back" | ||||
|     MEDIA_PLAYER = "mdi:volume-high" | ||||
|     TELEVISION = "mdi:television" | ||||
|     GAME_CONSOLE = "mdi:nintendo-game-boy" | ||||
|     STREAMING_DONGLE = "mdi:cast" | ||||
|     LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker" | ||||
|     DISC_PLAYER = "mdi:disk-player" | ||||
|     REMOTE_CONTROL = "mdi:remote-tv" | ||||
|     RADIO = "mdi:radio" | ||||
|     PHOTO_CAMERA = PHOTOS = "mdi:camera" | ||||
|     MICROPHONE = VOICE_CONTROL = "mdi:microphone" | ||||
|     PROJECTOR = "mdi:projector" | ||||
|     COMPUTER = DESKTOP = "mdi:desktop-tower" | ||||
|     LAPTOP = "mdi:laptop" | ||||
|     PRINTER = "mdi:printer" | ||||
|     SCANNER = "mdi:scanner" | ||||
|     POS = "mdi:printer-pos" | ||||
|     CLOCK = "mdi:clock" | ||||
|     BARCODE = "mdi:barcode" | ||||
|     SURVEILLANCE_CAMERA = BABY_MONITOR = PET_MONITOR = "mdi:cctv" | ||||
|     POE_PLUG = HEALTH_MONITOR = SMART_HOME = SMART_METER = APPLIANCE = SLEEP = ( | ||||
|         "mdi:home-automation" | ||||
|     ) | ||||
|     SMART_PLUG = "mdi:power-plug" | ||||
|     LIGHT = "mdi:lightbulb" | ||||
|     THERMOSTAT = HEATING = "mdi:home-thermometer" | ||||
|     POWER_SYSTEM = ENERGY = "mdi:lightning-bolt" | ||||
|     SOLAR_PANEL = "mdi:solar-power" | ||||
|     WASHER = "mdi:washing-machine" | ||||
|     FRIDGE = "mdi:fridge" | ||||
|     CLEANER = "mdi:vacuum" | ||||
|     GARAGE = "mdi:garage" | ||||
|     SPRINKLER = "mdi:sprinkler" | ||||
|     BELL = "mdi:doorbell" | ||||
|     KEY_LOCK = "mdi:lock-smart" | ||||
|     CONTROL_PANEL = SMART_CONTROLLER = "mdi:alarm-panel" | ||||
|     SCALE = "mdi:scale-bathroom" | ||||
|     TOY = "mdi:teddy-bear" | ||||
|     ROBOT = "mdi:robot" | ||||
|     WEATHER = "mdi:weather-cloudy" | ||||
|     ALARM = "mdi:alarm-light" | ||||
|     MOTION_DETECTOR = "mdi:motion-sensor" | ||||
|     SMOKE = HUMIDITY = SENSOR = DOMOTZ_BOX = FINGBOX = "mdi:smoke-detector" | ||||
|     ROUTER = MODEM = GATEWAY = FIREWALL = VPN = SMALL_CELL = "mdi:router-network" | ||||
|     WIFI = WIFI_EXTENDER = "mdi:wifi" | ||||
|     NAS_STORAGE = "mdi:nas" | ||||
|     SWITCH = "mdi:switch" | ||||
|     USB = "mdi:usb" | ||||
|     CLOUD = "mdi:cloud" | ||||
|     BATTERY = "mdi:battery" | ||||
|     NETWORK_APPLIANCE = "mdi:network" | ||||
|     VIRTUAL_MACHINE = MAIL_SERVER = FILE_SERVER = PROXY_SERVER = WEB_SERVER = ( | ||||
|         DOMAIN_SERVER | ||||
|     ) = COMMUNICATION = "mdi:monitor" | ||||
|     SERVER = "mdi:server" | ||||
|     TERMINAL = "mdi:console" | ||||
|     DATABASE = "mdi:database" | ||||
|     RASPBERRY = ARDUINO = "mdi:raspberry-pi" | ||||
|     PROCESSOR = CIRCUIT_CARD = RFID = "mdi:chip" | ||||
|     INDUSTRIAL = "mdi:factory" | ||||
|     MEDICAL = "mdi:medical-bag" | ||||
|     VOIP = CONFERENCING = "mdi:phone-voip" | ||||
|     FITNESS = "mdi:dumbbell" | ||||
|     POOL = "mdi:pool" | ||||
|     SECURITY_SYSTEM = "mdi:security" | ||||
|  | ||||
|  | ||||
| def get_icon_from_type(type: str) -> str: | ||||
|     """Return the right icon based on the type.""" | ||||
|     try: | ||||
|         return DeviceType[type].value | ||||
|     except (ValueError, KeyError): | ||||
|         return "mdi:lan-connect" | ||||
| @@ -1,26 +0,0 @@ | ||||
| """Diagnostics for the Firefly III integration.""" | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| from typing import Any | ||||
|  | ||||
| from homeassistant.components.diagnostics import async_redact_data | ||||
| from homeassistant.const import CONF_API_KEY, CONF_URL | ||||
| from homeassistant.core import HomeAssistant | ||||
|  | ||||
| from . import FireflyConfigEntry | ||||
| from .coordinator import FireflyDataUpdateCoordinator | ||||
|  | ||||
| TO_REDACT = [CONF_API_KEY, CONF_URL] | ||||
|  | ||||
|  | ||||
| async def async_get_config_entry_diagnostics( | ||||
|     hass: HomeAssistant, entry: FireflyConfigEntry | ||||
| ) -> dict[str, Any]: | ||||
|     """Return diagnostics for a config entry.""" | ||||
|     coordinator: FireflyDataUpdateCoordinator = entry.runtime_data | ||||
|  | ||||
|     return { | ||||
|         "config_entry": async_redact_data(entry.as_dict(), TO_REDACT), | ||||
|         "data": {"primary_currency": coordinator.data.primary_currency.to_dict()}, | ||||
|     } | ||||
| @@ -6,5 +6,5 @@ | ||||
|   "documentation": "https://www.home-assistant.io/integrations/firefly_iii", | ||||
|   "iot_class": "local_polling", | ||||
|   "quality_scale": "bronze", | ||||
|   "requirements": ["pyfirefly==0.1.8"] | ||||
|   "requirements": ["pyfirefly==0.1.6"] | ||||
| } | ||||
|   | ||||
| @@ -111,12 +111,7 @@ class FlumeConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|                 errors[CONF_PASSWORD] = "invalid_auth" | ||||
|  | ||||
|         return self.async_show_form( | ||||
|             step_id="user", | ||||
|             data_schema=DATA_SCHEMA, | ||||
|             errors=errors, | ||||
|             description_placeholders={ | ||||
|                 "api_url": "https://portal.flumetech.com/settings#token" | ||||
|             }, | ||||
|             step_id="user", data_schema=DATA_SCHEMA, errors=errors | ||||
|         ) | ||||
|  | ||||
|     async def async_step_reauth( | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at {api_url}", | ||||
|         "description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at https://portal.flumetech.com/settings#token", | ||||
|         "title": "Connect to your Flume account", | ||||
|         "data": { | ||||
|           "username": "[%key:common::config_flow::data::username%]", | ||||
|   | ||||
| @@ -14,7 +14,6 @@ from homeassistant.helpers import aiohttp_client | ||||
| from .const import DOMAIN | ||||
|  | ||||
| STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str}) | ||||
| API_KEY_URL = "https://freedompro.eu/" | ||||
|  | ||||
|  | ||||
| class Hub: | ||||
| @@ -54,11 +53,7 @@ class FreedomProConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|         """Show the setup form to the user.""" | ||||
|         if user_input is None: | ||||
|             return self.async_show_form( | ||||
|                 step_id="user", | ||||
|                 data_schema=STEP_USER_DATA_SCHEMA, | ||||
|                 description_placeholders={ | ||||
|                     "api_key_url": API_KEY_URL, | ||||
|                 }, | ||||
|                 step_id="user", data_schema=STEP_USER_DATA_SCHEMA | ||||
|             ) | ||||
|  | ||||
|         errors = {} | ||||
| @@ -73,12 +68,7 @@ class FreedomProConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|             return self.async_create_entry(title="Freedompro", data=user_input) | ||||
|  | ||||
|         return self.async_show_form( | ||||
|             step_id="user", | ||||
|             data_schema=STEP_USER_DATA_SCHEMA, | ||||
|             errors=errors, | ||||
|             description_placeholders={ | ||||
|                 "api_key_url": API_KEY_URL, | ||||
|             }, | ||||
|             step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors | ||||
|         ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]" | ||||
|         }, | ||||
|         "description": "Please enter the API key obtained from {api_key_url}", | ||||
|         "description": "Please enter the API key obtained from https://home.freedompro.eu", | ||||
|         "title": "Freedompro API key" | ||||
|       } | ||||
|     }, | ||||
|   | ||||
| @@ -2,7 +2,6 @@ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| from collections.abc import Callable, Mapping | ||||
| from dataclasses import dataclass, field | ||||
| from datetime import datetime, timedelta | ||||
| @@ -17,7 +16,6 @@ from fritzconnection.core.exceptions import ( | ||||
|     FritzConnectionException, | ||||
|     FritzSecurityError, | ||||
| ) | ||||
| from fritzconnection.lib.fritzcall import FritzCall | ||||
| from fritzconnection.lib.fritzhosts import FritzHosts | ||||
| from fritzconnection.lib.fritzstatus import FritzStatus | ||||
| from fritzconnection.lib.fritzwlan import FritzGuestWLAN | ||||
| @@ -122,7 +120,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): | ||||
|         self.fritz_guest_wifi: FritzGuestWLAN = None | ||||
|         self.fritz_hosts: FritzHosts = None | ||||
|         self.fritz_status: FritzStatus = None | ||||
|         self.fritz_call: FritzCall = None | ||||
|         self.host = host | ||||
|         self.mesh_role = MeshRoles.NONE | ||||
|         self.mesh_wifi_uplink = False | ||||
| @@ -186,7 +183,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): | ||||
|         self.fritz_hosts = FritzHosts(fc=self.connection) | ||||
|         self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection) | ||||
|         self.fritz_status = FritzStatus(fc=self.connection) | ||||
|         self.fritz_call = FritzCall(fc=self.connection) | ||||
|         info = self.fritz_status.get_device_info() | ||||
|  | ||||
|         _LOGGER.debug( | ||||
| @@ -621,14 +617,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): | ||||
|             self.fritz_guest_wifi.set_password, password, length | ||||
|         ) | ||||
|  | ||||
|     async def async_trigger_dial(self, number: str, max_ring_seconds: int) -> None: | ||||
|         """Trigger service to dial a number.""" | ||||
|         try: | ||||
|             await self.hass.async_add_executor_job(self.fritz_call.dial, number) | ||||
|             await asyncio.sleep(max_ring_seconds) | ||||
|         finally: | ||||
|             await self.hass.async_add_executor_job(self.fritz_call.hangup) | ||||
|  | ||||
|     async def async_trigger_cleanup(self) -> None: | ||||
|         """Trigger device trackers cleanup.""" | ||||
|         _LOGGER.debug("Device tracker cleanup triggered") | ||||
|   | ||||
| @@ -62,9 +62,6 @@ | ||||
|     }, | ||||
|     "set_guest_wifi_password": { | ||||
|       "service": "mdi:form-textbox-password" | ||||
|     }, | ||||
|     "dial": { | ||||
|       "service": "mdi:phone-dial" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ | ||||
|   "config_flow": true, | ||||
|   "dependencies": ["network"], | ||||
|   "documentation": "https://www.home-assistant.io/integrations/fritz", | ||||
|   "integration_type": "hub", | ||||
|   "iot_class": "local_polling", | ||||
|   "loggers": ["fritzconnection"], | ||||
|   "requirements": ["fritzconnection[qr]==1.15.0", "xmltodict==0.13.0"], | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import logging | ||||
|  | ||||
| from fritzconnection.core.exceptions import ( | ||||
|     FritzActionError, | ||||
|     FritzActionFailedError, | ||||
|     FritzConnectionException, | ||||
|     FritzServiceError, | ||||
| ) | ||||
| @@ -28,14 +27,6 @@ SERVICE_SCHEMA_SET_GUEST_WIFI_PW = vol.Schema( | ||||
|         vol.Optional("length"): vol.Range(min=8, max=63), | ||||
|     } | ||||
| ) | ||||
| SERVICE_DIAL = "dial" | ||||
| SERVICE_SCHEMA_DIAL = vol.Schema( | ||||
|     { | ||||
|         vol.Required("device_id"): str, | ||||
|         vol.Required("number"): str, | ||||
|         vol.Required("max_ring_seconds"): vol.Range(min=1, max=300), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None: | ||||
| @@ -74,46 +65,6 @@ async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None: | ||||
|             ) from ex | ||||
|  | ||||
|  | ||||
| async def _async_dial(service_call: ServiceCall) -> None: | ||||
|     """Call Fritz dial service.""" | ||||
|     target_entry_ids = await async_extract_config_entry_ids(service_call) | ||||
|     target_entries: list[FritzConfigEntry] = [ | ||||
|         loaded_entry | ||||
|         for loaded_entry in service_call.hass.config_entries.async_loaded_entries( | ||||
|             DOMAIN | ||||
|         ) | ||||
|         if loaded_entry.entry_id in target_entry_ids | ||||
|     ] | ||||
|  | ||||
|     if not target_entries: | ||||
|         raise ServiceValidationError( | ||||
|             translation_domain=DOMAIN, | ||||
|             translation_key="config_entry_not_found", | ||||
|             translation_placeholders={"service": service_call.service}, | ||||
|         ) | ||||
|  | ||||
|     for target_entry in target_entries: | ||||
|         _LOGGER.debug("Executing service %s", service_call.service) | ||||
|         avm_wrapper = target_entry.runtime_data | ||||
|         try: | ||||
|             await avm_wrapper.async_trigger_dial( | ||||
|                 service_call.data["number"], | ||||
|                 max_ring_seconds=service_call.data["max_ring_seconds"], | ||||
|             ) | ||||
|         except (FritzServiceError, FritzActionError) as ex: | ||||
|             raise HomeAssistantError( | ||||
|                 translation_domain=DOMAIN, translation_key="service_parameter_unknown" | ||||
|             ) from ex | ||||
|         except FritzActionFailedError as ex: | ||||
|             raise HomeAssistantError( | ||||
|                 translation_domain=DOMAIN, translation_key="service_dial_failed" | ||||
|             ) from ex | ||||
|         except FritzConnectionException as ex: | ||||
|             raise HomeAssistantError( | ||||
|                 translation_domain=DOMAIN, translation_key="service_not_supported" | ||||
|             ) from ex | ||||
|  | ||||
|  | ||||
| @callback | ||||
| def async_setup_services(hass: HomeAssistant) -> None: | ||||
|     """Set up services for Fritz integration.""" | ||||
| @@ -124,4 +75,3 @@ def async_setup_services(hass: HomeAssistant) -> None: | ||||
|         _async_set_guest_wifi_password, | ||||
|         SERVICE_SCHEMA_SET_GUEST_WIFI_PW, | ||||
|     ) | ||||
|     hass.services.async_register(DOMAIN, SERVICE_DIAL, _async_dial, SERVICE_SCHEMA_DIAL) | ||||
|   | ||||
| @@ -17,24 +17,3 @@ set_guest_wifi_password: | ||||
|         number: | ||||
|           min: 8 | ||||
|           max: 63 | ||||
| dial: | ||||
|   fields: | ||||
|     device_id: | ||||
|       required: true | ||||
|       selector: | ||||
|         device: | ||||
|           integration: fritz | ||||
|           entity: | ||||
|             device_class: connectivity | ||||
|     number: | ||||
|       required: true | ||||
|       selector: | ||||
|         text: | ||||
|     max_ring_seconds: | ||||
|       default: 15 | ||||
|       required: true | ||||
|       selector: | ||||
|         number: | ||||
|           min: 1 | ||||
|           max: 300 | ||||
|           unit_of_measurement: seconds | ||||
|   | ||||
| @@ -198,33 +198,12 @@ | ||||
|           "description": "Length of the new password. It will be auto-generated if no password is set." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "dial": { | ||||
|       "name": "Dial a phone number", | ||||
|       "description": "Makes the FRITZ!Box dial a phone number.", | ||||
|       "fields": { | ||||
|         "device_id": { | ||||
|           "name": "FRITZ!Box device", | ||||
|           "description": "Select the FRITZ!Box to dial from." | ||||
|         }, | ||||
|         "number": { | ||||
|           "name": "Phone number", | ||||
|           "description": "The phone number to dial." | ||||
|         }, | ||||
|         "max_ring_seconds": { | ||||
|           "name": "Maximum ring duration", | ||||
|           "description": "The maximum number of seconds to ring after dialing." | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "exceptions": { | ||||
|     "config_entry_not_found": { | ||||
|       "message": "Failed to perform action \"{service}\". Config entry for target not found" | ||||
|     }, | ||||
|     "service_dial_failed": { | ||||
|       "message": "Failed to dial, check if the click to dial service of the FRITZ!Box is activated" | ||||
|     }, | ||||
|     "service_parameter_unknown": { | ||||
|       "message": "Action or parameter unknown" | ||||
|     }, | ||||
|   | ||||
| @@ -128,7 +128,7 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|             self._abort_if_unique_id_configured() | ||||
|             return await self.async_step_confirm() | ||||
|  | ||||
|         current_addresses = self._async_current_ids(include_ignore=False) | ||||
|         current_addresses = self._async_current_ids() | ||||
|         for discovery_info in async_discovered_service_info(self.hass): | ||||
|             address = discovery_info.address | ||||
|             if address in current_addresses or not _is_supported(discovery_info): | ||||
|   | ||||
| @@ -101,7 +101,9 @@ class GeolocationEvent(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): | ||||
|     @property | ||||
|     def state_attributes(self) -> dict[str, Any]: | ||||
|         """Return the state attributes of this external event.""" | ||||
|         data: dict[str, Any] = {ATTR_SOURCE: self.source} | ||||
|         data: dict[str, Any] = self.generate_entity_state_attributes() | ||||
|  | ||||
|         data[ATTR_SOURCE] = self.source | ||||
|         if self.latitude is not None: | ||||
|             data[ATTR_LATITUDE] = round(self.latitude, 5) | ||||
|         if self.longitude is not None: | ||||
|   | ||||
| @@ -6,4 +6,4 @@ CONF_DEBUG_UI = "debug_ui" | ||||
| DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." | ||||
| HA_MANAGED_API_PORT = 11984 | ||||
| HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" | ||||
| RECOMMENDED_VERSION = "1.9.11" | ||||
| RECOMMENDED_VERSION = "1.9.9" | ||||
|   | ||||
| @@ -186,7 +186,6 @@ async def async_setup_entry( | ||||
| class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity): | ||||
|     """Entity representing individual inverter sensor.""" | ||||
|  | ||||
|     _attr_has_entity_name = True | ||||
|     entity_description: GoodweSensorEntityDescription | ||||
|  | ||||
|     def __init__( | ||||
|   | ||||
| @@ -15,13 +15,7 @@ from homeassistant.exceptions import HomeAssistantError | ||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||
| from homeassistant.util.json import json_loads | ||||
|  | ||||
| from .const import ( | ||||
|     CONF_CHAT_MODEL, | ||||
|     CONF_RECOMMENDED, | ||||
|     LOGGER, | ||||
|     RECOMMENDED_AI_TASK_MAX_TOKENS, | ||||
|     RECOMMENDED_IMAGE_MODEL, | ||||
| ) | ||||
| from .const import CONF_CHAT_MODEL, CONF_RECOMMENDED, LOGGER, RECOMMENDED_IMAGE_MODEL | ||||
| from .entity import ( | ||||
|     ERROR_GETTING_RESPONSE, | ||||
|     GoogleGenerativeAILLMBaseEntity, | ||||
| @@ -79,9 +73,7 @@ class GoogleGenerativeAITaskEntity( | ||||
|         chat_log: conversation.ChatLog, | ||||
|     ) -> ai_task.GenDataTaskResult: | ||||
|         """Handle a generate data task.""" | ||||
|         await self._async_handle_chat_log( | ||||
|             chat_log, task.structure, default_max_tokens=RECOMMENDED_AI_TASK_MAX_TOKENS | ||||
|         ) | ||||
|         await self._async_handle_chat_log(chat_log, task.structure) | ||||
|  | ||||
|         if not isinstance(chat_log.content[-1], conversation.AssistantContent): | ||||
|             LOGGER.error( | ||||
|   | ||||
| @@ -32,8 +32,6 @@ CONF_TOP_K = "top_k" | ||||
| RECOMMENDED_TOP_K = 64 | ||||
| CONF_MAX_TOKENS = "max_tokens" | ||||
| RECOMMENDED_MAX_TOKENS = 3000 | ||||
| # Input 5000, output 19400 = 0.05 USD | ||||
| RECOMMENDED_AI_TASK_MAX_TOKENS = 19400 | ||||
| CONF_HARASSMENT_BLOCK_THRESHOLD = "harassment_block_threshold" | ||||
| CONF_HATE_BLOCK_THRESHOLD = "hate_block_threshold" | ||||
| CONF_SEXUAL_BLOCK_THRESHOLD = "sexual_block_threshold" | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user