mirror of
				https://github.com/home-assistant/core.git
				synced 2025-10-31 06:29:31 +00:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			dev
			...
			zjs-config
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | fe35fac8ee | ||
|   | 4bccc57b46 | 
| @@ -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) | ||||
|  | ||||
|   | ||||
							
								
								
									
										54
									
								
								.github/workflows/builder.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										54
									
								
								.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 | ||||
| @@ -162,8 +162,20 @@ jobs: | ||||
|             sed -i "s|home-assistant-intents==.*||" requirements_all.txt | ||||
|           fi | ||||
|  | ||||
|       - name: Adjustments for armhf | ||||
|         if: matrix.arch == 'armhf' | ||||
|         run: | | ||||
|           # Pandas has issues building on armhf, it is expected they | ||||
|           # will drop the platform in the near future (they consider it | ||||
|           # "flimsy" on 386). The following packages depend on pandas, | ||||
|           # so we comment them out. | ||||
|           sed -i "s|env-canada|# env-canada|g" requirements_all.txt | ||||
|           sed -i "s|noaa-coops|# noaa-coops|g" requirements_all.txt | ||||
|           sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt | ||||
|           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 | ||||
|  | ||||
| @@ -214,11 +226,19 @@ jobs: | ||||
|           - odroid-c4 | ||||
|           - odroid-m1 | ||||
|           - odroid-n2 | ||||
|           - odroid-xu | ||||
|           - qemuarm | ||||
|           - qemuarm-64 | ||||
|           - qemux86 | ||||
|           - qemux86-64 | ||||
|           - raspberrypi | ||||
|           - raspberrypi2 | ||||
|           - raspberrypi3 | ||||
|           - raspberrypi3-64 | ||||
|           - raspberrypi4 | ||||
|           - raspberrypi4-64 | ||||
|           - raspberrypi5-64 | ||||
|           - tinker | ||||
|           - yellow | ||||
|           - green | ||||
|     steps: | ||||
| @@ -277,7 +297,6 @@ jobs: | ||||
|           key-description: "Home Assistant Core" | ||||
|           version: ${{ needs.init.outputs.version }} | ||||
|           channel: ${{ needs.init.outputs.channel }} | ||||
|           exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]' | ||||
|  | ||||
|       - name: Update version file (stable -> beta) | ||||
|         if: needs.init.outputs.channel == 'stable' | ||||
| @@ -287,7 +306,6 @@ jobs: | ||||
|           key-description: "Home Assistant Core" | ||||
|           version: ${{ needs.init.outputs.version }} | ||||
|           channel: beta | ||||
|           exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]' | ||||
|  | ||||
|   publish_container: | ||||
|     name: Publish meta container for ${{ matrix.registry }} | ||||
| @@ -308,7 +326,7 @@ jobs: | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - name: Install Cosign | ||||
|         uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 | ||||
|         uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 | ||||
|         with: | ||||
|           cosign-release: "v2.2.3" | ||||
|  | ||||
| @@ -339,12 +357,27 @@ jobs: | ||||
|  | ||||
|             docker manifest create "${registry}/home-assistant:${tag_l}" \ | ||||
|               "${registry}/amd64-homeassistant:${tag_r}" \ | ||||
|               "${registry}/i386-homeassistant:${tag_r}" \ | ||||
|               "${registry}/armhf-homeassistant:${tag_r}" \ | ||||
|               "${registry}/armv7-homeassistant:${tag_r}" \ | ||||
|               "${registry}/aarch64-homeassistant:${tag_r}" | ||||
|  | ||||
|             docker manifest annotate "${registry}/home-assistant:${tag_l}" \ | ||||
|               "${registry}/amd64-homeassistant:${tag_r}" \ | ||||
|               --os linux --arch amd64 | ||||
|  | ||||
|             docker manifest annotate "${registry}/home-assistant:${tag_l}" \ | ||||
|               "${registry}/i386-homeassistant:${tag_r}" \ | ||||
|               --os linux --arch 386 | ||||
|  | ||||
|             docker manifest annotate "${registry}/home-assistant:${tag_l}" \ | ||||
|               "${registry}/armhf-homeassistant:${tag_r}" \ | ||||
|               --os linux --arch arm --variant=v6 | ||||
|  | ||||
|             docker manifest annotate "${registry}/home-assistant:${tag_l}" \ | ||||
|               "${registry}/armv7-homeassistant:${tag_r}" \ | ||||
|               --os linux --arch arm --variant=v7 | ||||
|  | ||||
|             docker manifest annotate "${registry}/home-assistant:${tag_l}" \ | ||||
|               "${registry}/aarch64-homeassistant:${tag_r}" \ | ||||
|               --os linux --arch arm64 --variant=v8 | ||||
| @@ -372,14 +405,23 @@ jobs: | ||||
|  | ||||
|           # Pull images from github container registry and verify signature | ||||
|           docker pull "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}" | ||||
|           docker pull "ghcr.io/home-assistant/i386-homeassistant:${{ needs.init.outputs.version }}" | ||||
|           docker pull "ghcr.io/home-assistant/armhf-homeassistant:${{ needs.init.outputs.version }}" | ||||
|           docker pull "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}" | ||||
|           docker pull "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}" | ||||
|  | ||||
|           validate_image "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}" | ||||
|           validate_image "ghcr.io/home-assistant/i386-homeassistant:${{ needs.init.outputs.version }}" | ||||
|           validate_image "ghcr.io/home-assistant/armhf-homeassistant:${{ needs.init.outputs.version }}" | ||||
|           validate_image "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}" | ||||
|           validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}" | ||||
|  | ||||
|           if [[ "${{ matrix.registry }}" ==  "docker.io/homeassistant" ]]; then | ||||
|             # Upload images to dockerhub | ||||
|             push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}" | ||||
|             push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}" | ||||
|             push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}" | ||||
|             push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}" | ||||
|             push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}" | ||||
|           fi | ||||
|  | ||||
| @@ -422,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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										846
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										846
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										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@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 | ||||
|         with: | ||||
|           languages: python | ||||
|  | ||||
|       - name: Perform CodeQL Analysis | ||||
|         uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|         uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 | ||||
|         with: | ||||
|           category: "/language:python" | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|       # - No PRs marked as no-stale | ||||
|       # - No issues (-1) | ||||
|       - name: 60 days stale PRs policy | ||||
|         uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 | ||||
|         uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 | ||||
|         with: | ||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           days-before-stale: 60 | ||||
| @@ -57,7 +57,7 @@ jobs: | ||||
|       # - No issues marked as no-stale or help-wanted | ||||
|       # - No PRs (-1) | ||||
|       - name: 90 days stale issues | ||||
|         uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 | ||||
|         uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 | ||||
|         with: | ||||
|           repo-token: ${{ steps.token.outputs.token }} | ||||
|           days-before-stale: 90 | ||||
| @@ -87,7 +87,7 @@ jobs: | ||||
|       # - No Issues marked as no-stale or help-wanted | ||||
|       # - No PRs (-1) | ||||
|       - name: Needs more information stale issues policy | ||||
|         uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 | ||||
|         uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 | ||||
|         with: | ||||
|           repo-token: ${{ steps.token.outputs.token }} | ||||
|           only-labels: "needs-more-information" | ||||
|   | ||||
							
								
								
									
										80
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										80
									
								
								.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,14 +221,14 @@ 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 | ||||
|           arch: ${{ matrix.arch }} | ||||
|           wheels-key: ${{ secrets.WHEELS_KEY }} | ||||
|           env-file: true | ||||
|           apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev" | ||||
|           apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev" | ||||
|           skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl | ||||
|           constraints: "homeassistant/package_constraints.txt" | ||||
|           requirements-diff: "requirements_diff.txt" | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -79,6 +79,7 @@ junit.xml | ||||
| .project | ||||
| .pydevproject | ||||
|  | ||||
| .python-version | ||||
| .tool-versions | ||||
|  | ||||
| # emacs auto backups | ||||
| @@ -111,7 +112,6 @@ virtualization/vagrant/config | ||||
| !.vscode/cSpell.json | ||||
| !.vscode/extensions.json | ||||
| !.vscode/tasks.json | ||||
| !.vscode/settings.default.jsonc | ||||
| .env | ||||
|  | ||||
| # Windows Explorer | ||||
| @@ -141,5 +141,4 @@ pytest_buckets.txt | ||||
|  | ||||
| # AI tooling | ||||
| .claude/settings.local.json | ||||
| .serena/ | ||||
|  | ||||
|   | ||||
| @@ -33,13 +33,10 @@ repos: | ||||
|     rev: v1.37.1 | ||||
|     hooks: | ||||
|       - id: yamllint | ||||
|   - repo: https://github.com/rbubley/mirrors-prettier | ||||
|     rev: v3.6.2 | ||||
|   - repo: https://github.com/pre-commit/mirrors-prettier | ||||
|     rev: v3.0.3 | ||||
|     hooks: | ||||
|       - id: prettier | ||||
|         additional_dependencies: | ||||
|           - prettier@3.6.2 | ||||
|           - prettier-plugin-sort-json@4.1.1 | ||||
|   - repo: https://github.com/cdce8p/python-typing-update | ||||
|     rev: v0.6.0 | ||||
|     hooks: | ||||
|   | ||||
| @@ -1,24 +0,0 @@ | ||||
| /** @type {import("prettier").Config} */ | ||||
| module.exports = { | ||||
|   overrides: [ | ||||
|     { | ||||
|       files: "./homeassistant/**/*.json", | ||||
|       options: { | ||||
|         plugins: [require.resolve("prettier-plugin-sort-json")], | ||||
|         jsonRecursiveSort: true, | ||||
|         jsonSortOrder: JSON.stringify({ [/.*/]: "numeric" }), | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       files: ["manifest.json", "./**/brands/*.json"], | ||||
|       options: { | ||||
|         // domain and name should stay at the top | ||||
|         jsonSortOrder: JSON.stringify({ | ||||
|           domain: null, | ||||
|           name: null, | ||||
|           [/.*/]: "numeric", | ||||
|         }), | ||||
|       }, | ||||
|     }, | ||||
|   ], | ||||
| }; | ||||
| @@ -1 +0,0 @@ | ||||
| 3.13 | ||||
| @@ -107,7 +107,6 @@ homeassistant.components.automation.* | ||||
| homeassistant.components.awair.* | ||||
| homeassistant.components.axis.* | ||||
| homeassistant.components.azure_storage.* | ||||
| homeassistant.components.backblaze_b2.* | ||||
| homeassistant.components.backup.* | ||||
| homeassistant.components.baf.* | ||||
| homeassistant.components.bang_olufsen.* | ||||
| @@ -183,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.* | ||||
| @@ -221,7 +221,6 @@ homeassistant.components.generic_thermostat.* | ||||
| homeassistant.components.geo_location.* | ||||
| homeassistant.components.geocaching.* | ||||
| homeassistant.components.gios.* | ||||
| homeassistant.components.github.* | ||||
| homeassistant.components.glances.* | ||||
| homeassistant.components.go2rtc.* | ||||
| homeassistant.components.goalzero.* | ||||
| @@ -279,7 +278,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.* | ||||
| @@ -362,6 +360,7 @@ homeassistant.components.myuplink.* | ||||
| homeassistant.components.nam.* | ||||
| homeassistant.components.nanoleaf.* | ||||
| homeassistant.components.nasweb.* | ||||
| homeassistant.components.neato.* | ||||
| homeassistant.components.nest.* | ||||
| homeassistant.components.netatmo.* | ||||
| homeassistant.components.network.* | ||||
| @@ -395,6 +394,7 @@ homeassistant.components.otbr.* | ||||
| homeassistant.components.overkiz.* | ||||
| homeassistant.components.overseerr.* | ||||
| homeassistant.components.p1_monitor.* | ||||
| homeassistant.components.pandora.* | ||||
| homeassistant.components.panel_custom.* | ||||
| homeassistant.components.paperless_ngx.* | ||||
| homeassistant.components.peblar.* | ||||
| @@ -477,7 +477,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.* | ||||
| @@ -556,7 +555,6 @@ homeassistant.components.vacuum.* | ||||
| homeassistant.components.vallox.* | ||||
| homeassistant.components.valve.* | ||||
| homeassistant.components.velbus.* | ||||
| homeassistant.components.vivotek.* | ||||
| homeassistant.components.vlc_telnet.* | ||||
| homeassistant.components.vodafone_station.* | ||||
| homeassistant.components.volvo.* | ||||
|   | ||||
| @@ -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" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										40
									
								
								CODEOWNERS
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										40
									
								
								CODEOWNERS
									
									
									
										generated
									
									
									
								
							| @@ -46,8 +46,6 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/accuweather/ @bieniu | ||||
| /homeassistant/components/acmeda/ @atmurray | ||||
| /tests/components/acmeda/ @atmurray | ||||
| /homeassistant/components/actron_air/ @kclif9 @JagadishDhanamjayam | ||||
| /tests/components/actron_air/ @kclif9 @JagadishDhanamjayam | ||||
| /homeassistant/components/adax/ @danielhiversen @lazytarget | ||||
| /tests/components/adax/ @danielhiversen @lazytarget | ||||
| /homeassistant/components/adguard/ @frenck | ||||
| @@ -196,8 +194,6 @@ build.json @home-assistant/supervisor | ||||
| /homeassistant/components/azure_service_bus/ @hfurubotten | ||||
| /homeassistant/components/azure_storage/ @zweckj | ||||
| /tests/components/azure_storage/ @zweckj | ||||
| /homeassistant/components/backblaze_b2/ @hugo-vrijswijk @ElCruncharino | ||||
| /tests/components/backblaze_b2/ @hugo-vrijswijk @ElCruncharino | ||||
| /homeassistant/components/backup/ @home-assistant/core | ||||
| /tests/components/backup/ @home-assistant/core | ||||
| /homeassistant/components/baf/ @bdraco @jfroy | ||||
| @@ -318,6 +314,8 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/cpuspeed/ @fabaff | ||||
| /homeassistant/components/crownstone/ @Crownstone @RicArch97 | ||||
| /tests/components/crownstone/ @Crownstone @RicArch97 | ||||
| /homeassistant/components/cups/ @fabaff | ||||
| /tests/components/cups/ @fabaff | ||||
| /homeassistant/components/cync/ @Kinachi249 | ||||
| /tests/components/cync/ @Kinachi249 | ||||
| /homeassistant/components/daikin/ @fredrike | ||||
| @@ -494,8 +492,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 | ||||
| @@ -510,6 +506,8 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/fjaraskupan/ @elupus | ||||
| /homeassistant/components/flexit_bacnet/ @lellky @piotrbulinski | ||||
| /tests/components/flexit_bacnet/ @lellky @piotrbulinski | ||||
| /homeassistant/components/flick_electric/ @ZephireNZ | ||||
| /tests/components/flick_electric/ @ZephireNZ | ||||
| /homeassistant/components/flipr/ @cnico | ||||
| /tests/components/flipr/ @cnico | ||||
| /homeassistant/components/flo/ @dmulcahey | ||||
| @@ -619,8 +617,6 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/greeneye_monitor/ @jkeljo | ||||
| /homeassistant/components/group/ @home-assistant/core | ||||
| /tests/components/group/ @home-assistant/core | ||||
| /homeassistant/components/growatt_server/ @johanzander | ||||
| /tests/components/growatt_server/ @johanzander | ||||
| /homeassistant/components/guardian/ @bachya | ||||
| /tests/components/guardian/ @bachya | ||||
| /homeassistant/components/habitica/ @tr4nt0r | ||||
| @@ -741,8 +737,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 | ||||
| @@ -768,8 +762,8 @@ build.json @home-assistant/supervisor | ||||
| /homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz | ||||
| /tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz | ||||
| /homeassistant/components/intesishome/ @jnimmo | ||||
| /homeassistant/components/iometer/ @jukrebs | ||||
| /tests/components/iometer/ @jukrebs | ||||
| /homeassistant/components/iometer/ @MaestroOnICe | ||||
| /tests/components/iometer/ @MaestroOnICe | ||||
| /homeassistant/components/ios/ @robbiet480 | ||||
| /tests/components/ios/ @robbiet480 | ||||
| /homeassistant/components/iotawatt/ @gtdiehl @jyavenard | ||||
| @@ -1071,8 +1065,6 @@ build.json @home-assistant/supervisor | ||||
| /homeassistant/components/nilu/ @hfurubotten | ||||
| /homeassistant/components/nina/ @DeerMaximum | ||||
| /tests/components/nina/ @DeerMaximum | ||||
| /homeassistant/components/nintendo_parental_controls/ @pantherale0 | ||||
| /tests/components/nintendo_parental_controls/ @pantherale0 | ||||
| /homeassistant/components/nissan_leaf/ @filcole | ||||
| /homeassistant/components/noaa_tides/ @jdelaney72 | ||||
| /homeassistant/components/nobo_hub/ @echoromeo @oyvindwe | ||||
| @@ -1141,8 +1133,6 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/opengarage/ @danielhiversen | ||||
| /homeassistant/components/openhome/ @bazwilliams | ||||
| /tests/components/openhome/ @bazwilliams | ||||
| /homeassistant/components/openrgb/ @felipecrs | ||||
| /tests/components/openrgb/ @felipecrs | ||||
| /homeassistant/components/opensky/ @joostlek | ||||
| /tests/components/opensky/ @joostlek | ||||
| /homeassistant/components/opentherm_gw/ @mvn23 | ||||
| @@ -1206,6 +1196,8 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/plex/ @jjlawren | ||||
| /homeassistant/components/plugwise/ @CoMPaTech @bouwew | ||||
| /tests/components/plugwise/ @CoMPaTech @bouwew | ||||
| /homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa | ||||
| /tests/components/plum_lightpad/ @ColinHarrington @prystupa | ||||
| /homeassistant/components/point/ @fredrike | ||||
| /tests/components/point/ @fredrike | ||||
| /homeassistant/components/pooldose/ @lmaertin | ||||
| @@ -1421,8 +1413,8 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/sfr_box/ @epenet | ||||
| /homeassistant/components/sftp_storage/ @maretodoric | ||||
| /tests/components/sftp_storage/ @maretodoric | ||||
| /homeassistant/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre | ||||
| /tests/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre | ||||
| /homeassistant/components/sharkiq/ @JeffResc @funkybunch | ||||
| /tests/components/sharkiq/ @JeffResc @funkybunch | ||||
| /homeassistant/components/shell_command/ @home-assistant/core | ||||
| /tests/components/shell_command/ @home-assistant/core | ||||
| /homeassistant/components/shelly/ @bieniu @thecode @chemelli74 @bdraco | ||||
| @@ -1477,6 +1469,8 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/smhi/ @gjohansson-ST | ||||
| /homeassistant/components/smlight/ @tl-sl | ||||
| /tests/components/smlight/ @tl-sl | ||||
| /homeassistant/components/sms/ @ocalvo | ||||
| /tests/components/sms/ @ocalvo | ||||
| /homeassistant/components/snapcast/ @luar123 | ||||
| /tests/components/snapcast/ @luar123 | ||||
| /homeassistant/components/snmp/ @nmaggioni | ||||
| @@ -1485,8 +1479,8 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/snoo/ @Lash-L | ||||
| /homeassistant/components/snooz/ @AustinBrunkhorst | ||||
| /tests/components/snooz/ @AustinBrunkhorst | ||||
| /homeassistant/components/solaredge/ @frenck @bdraco @tronikos | ||||
| /tests/components/solaredge/ @frenck @bdraco @tronikos | ||||
| /homeassistant/components/solaredge/ @frenck @bdraco | ||||
| /tests/components/solaredge/ @frenck @bdraco | ||||
| /homeassistant/components/solaredge_local/ @drobtravels @scheric | ||||
| /homeassistant/components/solarlog/ @Ernst79 @dontinelli | ||||
| /tests/components/solarlog/ @Ernst79 @dontinelli | ||||
| @@ -1539,8 +1533,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 | ||||
| @@ -1717,8 +1709,8 @@ build.json @home-assistant/supervisor | ||||
| /tests/components/vallox/ @andre-richter @slovdahl @viiru- @yozik04 | ||||
| /homeassistant/components/valve/ @home-assistant/core | ||||
| /tests/components/valve/ @home-assistant/core | ||||
| /homeassistant/components/vegehub/ @thulrus | ||||
| /tests/components/vegehub/ @thulrus | ||||
| /homeassistant/components/vegehub/ @ghowevege | ||||
| /tests/components/vegehub/ @ghowevege | ||||
| /homeassistant/components/velbus/ @Cereal2nd @brefra | ||||
| /tests/components/velbus/ @Cereal2nd @brefra | ||||
| /homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew | ||||
|   | ||||
							
								
								
									
										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.6 | ||||
| RUN pip3 install uv==0.8.9 | ||||
|  | ||||
| WORKDIR /usr/src | ||||
|  | ||||
|   | ||||
| @@ -13,6 +13,7 @@ RUN \ | ||||
|         libavcodec-dev \ | ||||
|         libavdevice-dev \ | ||||
|         libavutil-dev \ | ||||
|         libgammu-dev \ | ||||
|         libswscale-dev \ | ||||
|         libswresample-dev \ | ||||
|         libavfilter-dev \ | ||||
| @@ -33,11 +34,9 @@ WORKDIR /usr/src | ||||
|  | ||||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv | ||||
|  | ||||
| RUN uv python install 3.13.2 | ||||
|  | ||||
| USER vscode | ||||
|  | ||||
| COPY .python-version ./ | ||||
| RUN uv python install | ||||
|  | ||||
| ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv" | ||||
| RUN uv venv $VIRTUAL_ENV | ||||
| ENV PATH="$VIRTUAL_ENV/bin:$PATH" | ||||
|   | ||||
							
								
								
									
										10
									
								
								build.yaml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								build.yaml
									
									
									
									
									
								
							| @@ -1,7 +1,13 @@ | ||||
| image: ghcr.io/home-assistant/{arch}-homeassistant | ||||
| build_from: | ||||
|   aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1 | ||||
|   amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1 | ||||
|   aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0 | ||||
|   armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0 | ||||
|   armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0 | ||||
|   amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0 | ||||
|   i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0 | ||||
| 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, | ||||
|         ) | ||||
|   | ||||
| @@ -635,6 +635,14 @@ async def async_enable_logging( | ||||
|         err_log_path = os.path.abspath(log_file) | ||||
|  | ||||
|     if err_log_path: | ||||
|         err_path_exists = os.path.isfile(err_log_path) | ||||
|         err_dir = os.path.dirname(err_log_path) | ||||
|  | ||||
|         # Check if we can write to the error log if it exists or that | ||||
|         # we can create files in the containing directory if not. | ||||
|         if (err_path_exists and os.access(err_log_path, os.W_OK)) or ( | ||||
|             not err_path_exists and os.access(err_dir, os.W_OK) | ||||
|         ): | ||||
|             err_handler = await hass.async_add_executor_job( | ||||
|                 _create_log_file, err_log_path, log_rotate_days | ||||
|             ) | ||||
| @@ -644,6 +652,8 @@ async def async_enable_logging( | ||||
|  | ||||
|             # Save the log file location for access by other components. | ||||
|             hass.data[DATA_LOGGING] = err_log_path | ||||
|         else: | ||||
|             _LOGGER.error("Unable to set up error log %s (access denied)", err_log_path) | ||||
|  | ||||
|     async_activate_log_queue_handler(hass) | ||||
|  | ||||
|   | ||||
							
								
								
									
										5
									
								
								homeassistant/brands/ibm.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								homeassistant/brands/ibm.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
|   "domain": "ibm", | ||||
|   "name": "IBM", | ||||
|   "integrations": ["watson_iot", "watson_tts"] | ||||
| } | ||||
| @@ -1,70 +1,70 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "invalid_mfa_code": "Invalid MFA code" | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "title": "Fill in your Abode login information", | ||||
|         "data": { | ||||
|           "username": "[%key:common::config_flow::data::email%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         } | ||||
|       }, | ||||
|       "mfa": { | ||||
|         "title": "Enter your MFA code for Abode", | ||||
|         "data": { | ||||
|           "mfa_code": "MFA code (6-digits)" | ||||
|         }, | ||||
|         "title": "Enter your MFA code for Abode" | ||||
|         } | ||||
|       }, | ||||
|       "reauth_confirm": { | ||||
|         "title": "[%key:component::abode::config::step::user::title%]", | ||||
|         "data": { | ||||
|           "password": "[%key:common::config_flow::data::password%]", | ||||
|           "username": "[%key:common::config_flow::data::email%]" | ||||
|         }, | ||||
|         "title": "[%key:component::abode::config::step::user::title%]" | ||||
|       }, | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "password": "[%key:common::config_flow::data::password%]", | ||||
|           "username": "[%key:common::config_flow::data::email%]" | ||||
|         }, | ||||
|         "title": "Fill in your Abode login information" | ||||
|           "username": "[%key:common::config_flow::data::email%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_mfa_code": "Invalid MFA code" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" | ||||
|     } | ||||
|   }, | ||||
|   "services": { | ||||
|     "capture_image": { | ||||
|       "name": "Capture image", | ||||
|       "description": "Requests a new image capture from a camera device.", | ||||
|       "fields": { | ||||
|         "entity_id": { | ||||
|           "description": "Entity ID of the camera to request an image from.", | ||||
|           "name": "Entity" | ||||
|           "name": "Entity", | ||||
|           "description": "Entity ID of the camera to request an image from." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|       "name": "Capture image" | ||||
|     }, | ||||
|     "change_setting": { | ||||
|       "name": "Change setting", | ||||
|       "description": "Changes an Abode system setting.", | ||||
|       "fields": { | ||||
|         "setting": { | ||||
|           "description": "Setting to change.", | ||||
|           "name": "Setting" | ||||
|           "name": "Setting", | ||||
|           "description": "Setting to change." | ||||
|         }, | ||||
|         "value": { | ||||
|           "description": "Value of the setting.", | ||||
|           "name": "Value" | ||||
|           "name": "Value", | ||||
|           "description": "Value of the setting." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|       "name": "Change setting" | ||||
|     }, | ||||
|     "trigger_automation": { | ||||
|       "name": "Trigger automation", | ||||
|       "description": "Triggers an Abode automation.", | ||||
|       "fields": { | ||||
|         "entity_id": { | ||||
|           "description": "Entity ID of the automation to trigger.", | ||||
|           "name": "Entity" | ||||
|           "name": "Entity", | ||||
|           "description": "Entity ID of the automation to trigger." | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|       "name": "Trigger automation" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -12,13 +12,11 @@ from homeassistant.components.bluetooth import async_get_scanner | ||||
| from homeassistant.config_entries import ConfigEntry | ||||
| from homeassistant.const import CONF_ADDRESS | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.debounce import Debouncer | ||||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator | ||||
|  | ||||
| from .const import CONF_IS_NEW_STYLE_SCALE | ||||
|  | ||||
| SCAN_INTERVAL = timedelta(seconds=15) | ||||
| UPDATE_DEBOUNCE_TIME = 0.2 | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -40,19 +38,11 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]): | ||||
|             config_entry=entry, | ||||
|         ) | ||||
|  | ||||
|         debouncer = Debouncer( | ||||
|             hass=hass, | ||||
|             logger=_LOGGER, | ||||
|             cooldown=UPDATE_DEBOUNCE_TIME, | ||||
|             immediate=True, | ||||
|             function=self.async_update_listeners, | ||||
|         ) | ||||
|  | ||||
|         self._scale = AcaiaScale( | ||||
|             address_or_ble_device=entry.data[CONF_ADDRESS], | ||||
|             name=entry.title, | ||||
|             is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE], | ||||
|             notify_callback=debouncer.async_schedule_call, | ||||
|             notify_callback=self.async_update_listeners, | ||||
|             scanner=async_get_scanner(hass), | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -4,20 +4,20 @@ | ||||
|       "timer_running": { | ||||
|         "default": "mdi:timer", | ||||
|         "state": { | ||||
|           "off": "mdi:timer-off", | ||||
|           "on": "mdi:timer-play" | ||||
|           "on": "mdi:timer-play", | ||||
|           "off": "mdi:timer-off" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "button": { | ||||
|       "tare": { | ||||
|         "default": "mdi:scale-balance" | ||||
|       }, | ||||
|       "reset_timer": { | ||||
|         "default": "mdi:timer-refresh" | ||||
|       }, | ||||
|       "start_stop": { | ||||
|         "default": "mdi:timer-play" | ||||
|       }, | ||||
|       "tare": { | ||||
|         "default": "mdi:scale-balance" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "flow_title": "{name}", | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", | ||||
| @@ -9,19 +10,18 @@ | ||||
|       "device_not_found": "Device could not be found.", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "flow_title": "{name}", | ||||
|     "step": { | ||||
|       "bluetooth_confirm": { | ||||
|         "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" | ||||
|       }, | ||||
|       "user": { | ||||
|         "description": "[%key:component::bluetooth::config::step::user::description%]", | ||||
|         "data": { | ||||
|           "address": "[%key:common::config_flow::data::device%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "address": "Select Acaia scale you want to set up" | ||||
|         }, | ||||
|         "description": "[%key:component::bluetooth::config::step::user::description%]" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @@ -32,14 +32,14 @@ | ||||
|       } | ||||
|     }, | ||||
|     "button": { | ||||
|       "tare": { | ||||
|         "name": "Tare" | ||||
|       }, | ||||
|       "reset_timer": { | ||||
|         "name": "Reset timer" | ||||
|       }, | ||||
|       "start_stop": { | ||||
|         "name": "Start/stop timer" | ||||
|       }, | ||||
|       "tare": { | ||||
|         "name": "Tare" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -71,4 +71,4 @@ POLLEN_CATEGORY_MAP = { | ||||
| } | ||||
| UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10) | ||||
| UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6) | ||||
| UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(minutes=30) | ||||
| UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30) | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| { | ||||
|   "entity": { | ||||
|     "sensor": { | ||||
|       "air_quality": { | ||||
|         "default": "mdi:air-filter" | ||||
|       }, | ||||
|       "cloud_ceiling": { | ||||
|         "default": "mdi:weather-fog" | ||||
|       }, | ||||
| @@ -37,6 +34,9 @@ | ||||
|       "thunderstorm_probability_night": { | ||||
|         "default": "mdi:weather-lightning" | ||||
|       }, | ||||
|       "translation_key": { | ||||
|         "default": "mdi:air-filter" | ||||
|       }, | ||||
|       "tree_pollen": { | ||||
|         "default": "mdi:tree-outline" | ||||
|       }, | ||||
|   | ||||
| @@ -1,8 +1,25 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "name": "[%key:common::config_flow::data::name%]", | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]", | ||||
|           "latitude": "[%key:common::config_flow::data::latitude%]", | ||||
|           "longitude": "[%key:common::config_flow::data::longitude%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "api_key": "API key generated in the AccuWeather APIs portal." | ||||
|         } | ||||
|       }, | ||||
|       "reauth_confirm": { | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "api_key": "[%key:component::accuweather::config::step::user::data_description::api_key%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "create_entry": { | ||||
|       "default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration." | ||||
| @@ -12,26 +29,9 @@ | ||||
|       "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", | ||||
|       "requests_exceeded": "The allowed number of requests to the AccuWeather API has been exceeded. You have to wait or change the API key." | ||||
|     }, | ||||
|     "step": { | ||||
|       "reauth_confirm": { | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "api_key": "[%key:component::accuweather::config::step::user::data_description::api_key%]" | ||||
|         } | ||||
|       }, | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]", | ||||
|           "latitude": "[%key:common::config_flow::data::latitude%]", | ||||
|           "longitude": "[%key:common::config_flow::data::longitude%]", | ||||
|           "name": "[%key:common::config_flow::data::name%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "api_key": "API key generated in the AccuWeather APIs portal." | ||||
|         } | ||||
|       } | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
| @@ -120,9 +120,9 @@ | ||||
|       "pressure_tendency": { | ||||
|         "name": "Pressure tendency", | ||||
|         "state": { | ||||
|           "falling": "Falling", | ||||
|           "steady": "Steady", | ||||
|           "rising": "Rising", | ||||
|           "steady": "Steady" | ||||
|           "falling": "Falling" | ||||
|         }, | ||||
|         "state_attributes": { | ||||
|           "options": { | ||||
| @@ -227,6 +227,9 @@ | ||||
|       "wet_bulb_temperature": { | ||||
|         "name": "Wet bulb temperature" | ||||
|       }, | ||||
|       "wind_speed": { | ||||
|         "name": "[%key:component::weather::entity_component::_::state_attributes::wind_speed::name%]" | ||||
|       }, | ||||
|       "wind_chill_temperature": { | ||||
|         "name": "Wind chill temperature" | ||||
|       }, | ||||
| @@ -239,9 +242,6 @@ | ||||
|       "wind_gust_speed_night": { | ||||
|         "name": "Wind gust speed night {forecast_day}" | ||||
|       }, | ||||
|       "wind_speed": { | ||||
|         "name": "[%key:component::weather::entity_component::_::state_attributes::wind_speed::name%]" | ||||
|       }, | ||||
|       "wind_speed_day": { | ||||
|         "name": "Wind speed day {forecast_day}" | ||||
|       }, | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "title": "Pick a hub to add", | ||||
|         "data": { | ||||
|           "id": "Host ID" | ||||
|         }, | ||||
|         "title": "Pick a hub to add" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "abort": { | ||||
|       "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,57 +0,0 @@ | ||||
| """The Actron Air integration.""" | ||||
|  | ||||
| from actron_neo_api import ( | ||||
|     ActronAirNeoACSystem, | ||||
|     ActronNeoAPI, | ||||
|     ActronNeoAPIError, | ||||
|     ActronNeoAuthError, | ||||
| ) | ||||
|  | ||||
| from homeassistant.const import CONF_API_TOKEN, Platform | ||||
| from homeassistant.core import HomeAssistant | ||||
|  | ||||
| from .const import _LOGGER | ||||
| from .coordinator import ( | ||||
|     ActronAirConfigEntry, | ||||
|     ActronAirRuntimeData, | ||||
|     ActronAirSystemCoordinator, | ||||
| ) | ||||
|  | ||||
| PLATFORM = [Platform.CLIMATE] | ||||
|  | ||||
|  | ||||
| async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool: | ||||
|     """Set up Actron Air integration from a config entry.""" | ||||
|  | ||||
|     api = ActronNeoAPI(refresh_token=entry.data[CONF_API_TOKEN]) | ||||
|     systems: list[ActronAirNeoACSystem] = [] | ||||
|  | ||||
|     try: | ||||
|         systems = await api.get_ac_systems() | ||||
|         await api.update_status() | ||||
|     except ActronNeoAuthError: | ||||
|         _LOGGER.error("Authentication error while setting up Actron Air integration") | ||||
|         raise | ||||
|     except ActronNeoAPIError as err: | ||||
|         _LOGGER.error("API error while setting up Actron Air integration: %s", err) | ||||
|         raise | ||||
|  | ||||
|     system_coordinators: dict[str, ActronAirSystemCoordinator] = {} | ||||
|     for system in systems: | ||||
|         coordinator = ActronAirSystemCoordinator(hass, entry, api, system) | ||||
|         _LOGGER.debug("Setting up coordinator for system: %s", system["serial"]) | ||||
|         await coordinator.async_config_entry_first_refresh() | ||||
|         system_coordinators[system["serial"]] = coordinator | ||||
|  | ||||
|     entry.runtime_data = ActronAirRuntimeData( | ||||
|         api=api, | ||||
|         system_coordinators=system_coordinators, | ||||
|     ) | ||||
|  | ||||
|     await hass.config_entries.async_forward_entry_setups(entry, PLATFORM) | ||||
|     return True | ||||
|  | ||||
|  | ||||
| async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool: | ||||
|     """Unload a config entry.""" | ||||
|     return await hass.config_entries.async_unload_platforms(entry, PLATFORM) | ||||
| @@ -1,259 +0,0 @@ | ||||
| """Climate platform for Actron Air integration.""" | ||||
|  | ||||
| from typing import Any | ||||
|  | ||||
| from actron_neo_api import ActronAirNeoStatus, ActronAirNeoZone | ||||
|  | ||||
| from homeassistant.components.climate import ( | ||||
|     FAN_AUTO, | ||||
|     FAN_HIGH, | ||||
|     FAN_LOW, | ||||
|     FAN_MEDIUM, | ||||
|     ClimateEntity, | ||||
|     ClimateEntityFeature, | ||||
|     HVACMode, | ||||
| ) | ||||
| from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.device_registry import DeviceInfo | ||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||
| from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||||
|  | ||||
| from .const import DOMAIN | ||||
| from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator | ||||
|  | ||||
| PARALLEL_UPDATES = 0 | ||||
|  | ||||
| FAN_MODE_MAPPING_ACTRONAIR_TO_HA = { | ||||
|     "AUTO": FAN_AUTO, | ||||
|     "LOW": FAN_LOW, | ||||
|     "MED": FAN_MEDIUM, | ||||
|     "HIGH": FAN_HIGH, | ||||
| } | ||||
| FAN_MODE_MAPPING_HA_TO_ACTRONAIR = { | ||||
|     v: k for k, v in FAN_MODE_MAPPING_ACTRONAIR_TO_HA.items() | ||||
| } | ||||
| HVAC_MODE_MAPPING_ACTRONAIR_TO_HA = { | ||||
|     "COOL": HVACMode.COOL, | ||||
|     "HEAT": HVACMode.HEAT, | ||||
|     "FAN": HVACMode.FAN_ONLY, | ||||
|     "AUTO": HVACMode.AUTO, | ||||
|     "OFF": HVACMode.OFF, | ||||
| } | ||||
| HVAC_MODE_MAPPING_HA_TO_ACTRONAIR = { | ||||
|     v: k for k, v in HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.items() | ||||
| } | ||||
|  | ||||
|  | ||||
| async def async_setup_entry( | ||||
|     hass: HomeAssistant, | ||||
|     entry: ActronAirConfigEntry, | ||||
|     async_add_entities: AddConfigEntryEntitiesCallback, | ||||
| ) -> None: | ||||
|     """Set up Actron Air climate entities.""" | ||||
|     system_coordinators = entry.runtime_data.system_coordinators | ||||
|     entities: list[ClimateEntity] = [] | ||||
|  | ||||
|     for coordinator in system_coordinators.values(): | ||||
|         status = coordinator.data | ||||
|         name = status.ac_system.system_name | ||||
|         entities.append(ActronSystemClimate(coordinator, name)) | ||||
|  | ||||
|         entities.extend( | ||||
|             ActronZoneClimate(coordinator, zone) | ||||
|             for zone in status.remote_zone_info | ||||
|             if zone.exists | ||||
|         ) | ||||
|  | ||||
|     async_add_entities(entities) | ||||
|  | ||||
|  | ||||
| class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEntity): | ||||
|     """Base class for Actron Air climate entities.""" | ||||
|  | ||||
|     _attr_has_entity_name = True | ||||
|     _attr_temperature_unit = UnitOfTemperature.CELSIUS | ||||
|     _attr_supported_features = ( | ||||
|         ClimateEntityFeature.TARGET_TEMPERATURE | ||||
|         | ClimateEntityFeature.FAN_MODE | ||||
|         | ClimateEntityFeature.TURN_ON | ||||
|         | ClimateEntityFeature.TURN_OFF | ||||
|     ) | ||||
|     _attr_name = None | ||||
|     _attr_fan_modes = list(FAN_MODE_MAPPING_ACTRONAIR_TO_HA.values()) | ||||
|     _attr_hvac_modes = list(HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.values()) | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         coordinator: ActronAirSystemCoordinator, | ||||
|         name: str, | ||||
|     ) -> None: | ||||
|         """Initialize an Actron Air unit.""" | ||||
|         super().__init__(coordinator) | ||||
|         self._serial_number = coordinator.serial_number | ||||
|  | ||||
|  | ||||
| class ActronSystemClimate(BaseClimateEntity): | ||||
|     """Representation of the Actron Air system.""" | ||||
|  | ||||
|     _attr_supported_features = ( | ||||
|         ClimateEntityFeature.TARGET_TEMPERATURE | ||||
|         | ClimateEntityFeature.FAN_MODE | ||||
|         | ClimateEntityFeature.TURN_ON | ||||
|         | ClimateEntityFeature.TURN_OFF | ||||
|     ) | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         coordinator: ActronAirSystemCoordinator, | ||||
|         name: str, | ||||
|     ) -> None: | ||||
|         """Initialize an Actron Air unit.""" | ||||
|         super().__init__(coordinator, name) | ||||
|         serial_number = coordinator.serial_number | ||||
|         self._attr_unique_id = serial_number | ||||
|         self._attr_device_info = DeviceInfo( | ||||
|             identifiers={(DOMAIN, serial_number)}, | ||||
|             name=self._status.ac_system.system_name, | ||||
|             manufacturer="Actron Air", | ||||
|             model_id=self._status.ac_system.master_wc_model, | ||||
|             sw_version=self._status.ac_system.master_wc_firmware_version, | ||||
|             serial_number=serial_number, | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def min_temp(self) -> float: | ||||
|         """Return the minimum temperature that can be set.""" | ||||
|         return self._status.min_temp | ||||
|  | ||||
|     @property | ||||
|     def max_temp(self) -> float: | ||||
|         """Return the maximum temperature that can be set.""" | ||||
|         return self._status.max_temp | ||||
|  | ||||
|     @property | ||||
|     def _status(self) -> ActronAirNeoStatus: | ||||
|         """Get the current status from the coordinator.""" | ||||
|         return self.coordinator.data | ||||
|  | ||||
|     @property | ||||
|     def hvac_mode(self) -> HVACMode | None: | ||||
|         """Return the current HVAC mode.""" | ||||
|         if not self._status.user_aircon_settings.is_on: | ||||
|             return HVACMode.OFF | ||||
|  | ||||
|         mode = self._status.user_aircon_settings.mode | ||||
|         return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode) | ||||
|  | ||||
|     @property | ||||
|     def fan_mode(self) -> str | None: | ||||
|         """Return the current fan mode.""" | ||||
|         fan_mode = self._status.user_aircon_settings.fan_mode | ||||
|         return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode) | ||||
|  | ||||
|     @property | ||||
|     def current_humidity(self) -> float: | ||||
|         """Return the current humidity.""" | ||||
|         return self._status.master_info.live_humidity_pc | ||||
|  | ||||
|     @property | ||||
|     def current_temperature(self) -> float: | ||||
|         """Return the current temperature.""" | ||||
|         return self._status.master_info.live_temp_c | ||||
|  | ||||
|     @property | ||||
|     def target_temperature(self) -> float: | ||||
|         """Return the target temperature.""" | ||||
|         return self._status.user_aircon_settings.temperature_setpoint_cool_c | ||||
|  | ||||
|     async def async_set_fan_mode(self, fan_mode: str) -> None: | ||||
|         """Set a new fan mode.""" | ||||
|         api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode.lower()) | ||||
|         await self._status.user_aircon_settings.set_fan_mode(api_fan_mode) | ||||
|  | ||||
|     async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: | ||||
|         """Set the HVAC mode.""" | ||||
|         ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode) | ||||
|         await self._status.ac_system.set_system_mode(ac_mode) | ||||
|  | ||||
|     async def async_set_temperature(self, **kwargs: Any) -> None: | ||||
|         """Set the temperature.""" | ||||
|         temp = kwargs.get(ATTR_TEMPERATURE) | ||||
|         await self._status.user_aircon_settings.set_temperature(temperature=temp) | ||||
|  | ||||
|  | ||||
| class ActronZoneClimate(BaseClimateEntity): | ||||
|     """Representation of a zone within the Actron Air system.""" | ||||
|  | ||||
|     _attr_supported_features = ( | ||||
|         ClimateEntityFeature.TARGET_TEMPERATURE | ||||
|         | ClimateEntityFeature.TURN_ON | ||||
|         | ClimateEntityFeature.TURN_OFF | ||||
|     ) | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         coordinator: ActronAirSystemCoordinator, | ||||
|         zone: ActronAirNeoZone, | ||||
|     ) -> None: | ||||
|         """Initialize an Actron Air unit.""" | ||||
|         super().__init__(coordinator, zone.title) | ||||
|         serial_number = coordinator.serial_number | ||||
|         self._zone_id: int = zone.zone_id | ||||
|         self._attr_unique_id: str = f"{serial_number}_zone_{zone.zone_id}" | ||||
|         self._attr_device_info: DeviceInfo = DeviceInfo( | ||||
|             identifiers={(DOMAIN, self._attr_unique_id)}, | ||||
|             name=zone.title, | ||||
|             manufacturer="Actron Air", | ||||
|             model="Zone", | ||||
|             suggested_area=zone.title, | ||||
|             via_device=(DOMAIN, serial_number), | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def min_temp(self) -> float: | ||||
|         """Return the minimum temperature that can be set.""" | ||||
|         return self._zone.min_temp | ||||
|  | ||||
|     @property | ||||
|     def max_temp(self) -> float: | ||||
|         """Return the maximum temperature that can be set.""" | ||||
|         return self._zone.max_temp | ||||
|  | ||||
|     @property | ||||
|     def _zone(self) -> ActronAirNeoZone: | ||||
|         """Get the current zone data from the coordinator.""" | ||||
|         status = self.coordinator.data | ||||
|         return status.zones[self._zone_id] | ||||
|  | ||||
|     @property | ||||
|     def hvac_mode(self) -> HVACMode | None: | ||||
|         """Return the current HVAC mode.""" | ||||
|         if self._zone.is_active: | ||||
|             mode = self._zone.hvac_mode | ||||
|             return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode) | ||||
|         return HVACMode.OFF | ||||
|  | ||||
|     @property | ||||
|     def current_humidity(self) -> float | None: | ||||
|         """Return the current humidity.""" | ||||
|         return self._zone.humidity | ||||
|  | ||||
|     @property | ||||
|     def current_temperature(self) -> float | None: | ||||
|         """Return the current temperature.""" | ||||
|         return self._zone.live_temp_c | ||||
|  | ||||
|     @property | ||||
|     def target_temperature(self) -> float | None: | ||||
|         """Return the target temperature.""" | ||||
|         return self._zone.temperature_setpoint_cool_c | ||||
|  | ||||
|     async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: | ||||
|         """Set the HVAC mode.""" | ||||
|         is_enabled = hvac_mode != HVACMode.OFF | ||||
|         await self._zone.enable(is_enabled) | ||||
|  | ||||
|     async def async_set_temperature(self, **kwargs: Any) -> None: | ||||
|         """Set the temperature.""" | ||||
|         await self._zone.set_temperature(temperature=kwargs["temperature"]) | ||||
| @@ -1,132 +0,0 @@ | ||||
| """Setup config flow for Actron Air integration.""" | ||||
|  | ||||
| import asyncio | ||||
| from typing import Any | ||||
|  | ||||
| from actron_neo_api import ActronNeoAPI, ActronNeoAuthError | ||||
|  | ||||
| from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||||
| from homeassistant.const import CONF_API_TOKEN | ||||
| from homeassistant.exceptions import HomeAssistantError | ||||
|  | ||||
| from .const import _LOGGER, DOMAIN | ||||
|  | ||||
|  | ||||
| class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|     """Handle a config flow for Actron Air.""" | ||||
|  | ||||
|     def __init__(self) -> None: | ||||
|         """Initialize the config flow.""" | ||||
|         self._api: ActronNeoAPI | None = None | ||||
|         self._device_code: str | None = None | ||||
|         self._user_code: str = "" | ||||
|         self._verification_uri: str = "" | ||||
|         self._expires_minutes: str = "30" | ||||
|         self.login_task: asyncio.Task | None = None | ||||
|  | ||||
|     async def async_step_user( | ||||
|         self, user_input: dict[str, Any] | None = None | ||||
|     ) -> ConfigFlowResult: | ||||
|         """Handle the initial step.""" | ||||
|         if self._api is None: | ||||
|             _LOGGER.debug("Initiating device authorization") | ||||
|             self._api = ActronNeoAPI() | ||||
|             try: | ||||
|                 device_code_response = await self._api.request_device_code() | ||||
|             except ActronNeoAuthError as err: | ||||
|                 _LOGGER.error("OAuth2 flow failed: %s", err) | ||||
|                 return self.async_abort(reason="oauth2_error") | ||||
|  | ||||
|             self._device_code = device_code_response["device_code"] | ||||
|             self._user_code = device_code_response["user_code"] | ||||
|             self._verification_uri = device_code_response["verification_uri_complete"] | ||||
|             self._expires_minutes = str(device_code_response["expires_in"] // 60) | ||||
|  | ||||
|         async def _wait_for_authorization() -> None: | ||||
|             """Wait for the user to authorize the device.""" | ||||
|             assert self._api is not None | ||||
|             assert self._device_code is not None | ||||
|             _LOGGER.debug("Waiting for device authorization") | ||||
|             try: | ||||
|                 await self._api.poll_for_token(self._device_code) | ||||
|                 _LOGGER.debug("Authorization successful") | ||||
|             except ActronNeoAuthError as ex: | ||||
|                 _LOGGER.exception("Error while waiting for device authorization") | ||||
|                 raise CannotConnect from ex | ||||
|  | ||||
|         _LOGGER.debug("Checking login task") | ||||
|         if self.login_task is None: | ||||
|             _LOGGER.debug("Creating task for device authorization") | ||||
|             self.login_task = self.hass.async_create_task(_wait_for_authorization()) | ||||
|  | ||||
|         if self.login_task.done(): | ||||
|             _LOGGER.debug("Login task is done, checking results") | ||||
|             if exception := self.login_task.exception(): | ||||
|                 if isinstance(exception, CannotConnect): | ||||
|                     return self.async_show_progress_done( | ||||
|                         next_step_id="connection_error" | ||||
|                     ) | ||||
|                 return self.async_show_progress_done(next_step_id="timeout") | ||||
|             return self.async_show_progress_done(next_step_id="finish_login") | ||||
|  | ||||
|         return self.async_show_progress( | ||||
|             step_id="user", | ||||
|             progress_action="wait_for_authorization", | ||||
|             description_placeholders={ | ||||
|                 "user_code": self._user_code, | ||||
|                 "verification_uri": self._verification_uri, | ||||
|                 "expires_minutes": self._expires_minutes, | ||||
|             }, | ||||
|             progress_task=self.login_task, | ||||
|         ) | ||||
|  | ||||
|     async def async_step_finish_login( | ||||
|         self, user_input: dict[str, Any] | None = None | ||||
|     ) -> ConfigFlowResult: | ||||
|         """Handle the finalization of login.""" | ||||
|         _LOGGER.debug("Finalizing authorization") | ||||
|         assert self._api is not None | ||||
|  | ||||
|         try: | ||||
|             user_data = await self._api.get_user_info() | ||||
|         except ActronNeoAuthError as err: | ||||
|             _LOGGER.error("Error getting user info: %s", err) | ||||
|             return self.async_abort(reason="oauth2_error") | ||||
|  | ||||
|         unique_id = str(user_data["id"]) | ||||
|         await self.async_set_unique_id(unique_id) | ||||
|         self._abort_if_unique_id_configured() | ||||
|  | ||||
|         return self.async_create_entry( | ||||
|             title=user_data["email"], | ||||
|             data={CONF_API_TOKEN: self._api.refresh_token_value}, | ||||
|         ) | ||||
|  | ||||
|     async def async_step_timeout( | ||||
|         self, | ||||
|         user_input: dict[str, Any] | None = None, | ||||
|     ) -> ConfigFlowResult: | ||||
|         """Handle issues that need transition await from progress step.""" | ||||
|         if user_input is None: | ||||
|             return self.async_show_form( | ||||
|                 step_id="timeout", | ||||
|             ) | ||||
|         del self.login_task | ||||
|         return await self.async_step_user() | ||||
|  | ||||
|     async def async_step_connection_error( | ||||
|         self, user_input: dict[str, Any] | None = None | ||||
|     ) -> ConfigFlowResult: | ||||
|         """Handle connection error from progress step.""" | ||||
|         if user_input is None: | ||||
|             return self.async_show_form(step_id="connection_error") | ||||
|  | ||||
|         # Reset state and try again | ||||
|         self._api = None | ||||
|         self._device_code = None | ||||
|         self.login_task = None | ||||
|         return await self.async_step_user() | ||||
|  | ||||
|  | ||||
| class CannotConnect(HomeAssistantError): | ||||
|     """Error to indicate we cannot connect.""" | ||||
| @@ -1,6 +0,0 @@ | ||||
| """Constants used by Actron Air integration.""" | ||||
|  | ||||
| import logging | ||||
|  | ||||
| _LOGGER = logging.getLogger(__package__) | ||||
| DOMAIN = "actron_air" | ||||
| @@ -1,69 +0,0 @@ | ||||
| """Coordinator for Actron Air integration.""" | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| from dataclasses import dataclass | ||||
| from datetime import timedelta | ||||
|  | ||||
| from actron_neo_api import ActronAirNeoACSystem, ActronAirNeoStatus, ActronNeoAPI | ||||
|  | ||||
| from homeassistant.config_entries import ConfigEntry | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator | ||||
| from homeassistant.util import dt as dt_util | ||||
|  | ||||
| from .const import _LOGGER | ||||
|  | ||||
| STALE_DEVICE_TIMEOUT = timedelta(hours=24) | ||||
| ERROR_NO_SYSTEMS_FOUND = "no_systems_found" | ||||
| ERROR_UNKNOWN = "unknown_error" | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class ActronAirRuntimeData: | ||||
|     """Runtime data for the Actron Air integration.""" | ||||
|  | ||||
|     api: ActronNeoAPI | ||||
|     system_coordinators: dict[str, ActronAirSystemCoordinator] | ||||
|  | ||||
|  | ||||
| type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData] | ||||
|  | ||||
| AUTH_ERROR_THRESHOLD = 3 | ||||
| SCAN_INTERVAL = timedelta(seconds=30) | ||||
|  | ||||
|  | ||||
| class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirNeoACSystem]): | ||||
|     """System coordinator for Actron Air integration.""" | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         hass: HomeAssistant, | ||||
|         entry: ActronAirConfigEntry, | ||||
|         api: ActronNeoAPI, | ||||
|         system: ActronAirNeoACSystem, | ||||
|     ) -> None: | ||||
|         """Initialize the coordinator.""" | ||||
|         super().__init__( | ||||
|             hass, | ||||
|             _LOGGER, | ||||
|             name="Actron Air Status", | ||||
|             update_interval=SCAN_INTERVAL, | ||||
|             config_entry=entry, | ||||
|         ) | ||||
|         self.system = system | ||||
|         self.serial_number = system["serial"] | ||||
|         self.api = api | ||||
|         self.status = self.api.state_manager.get_status(self.serial_number) | ||||
|         self.last_seen = dt_util.utcnow() | ||||
|  | ||||
|     async def _async_update_data(self) -> ActronAirNeoStatus: | ||||
|         """Fetch updates and merge incremental changes into the full state.""" | ||||
|         await self.api.update_status() | ||||
|         self.status = self.api.state_manager.get_status(self.serial_number) | ||||
|         self.last_seen = dt_util.utcnow() | ||||
|         return self.status | ||||
|  | ||||
|     def is_device_stale(self) -> bool: | ||||
|         """Check if a device is stale (not seen for a while).""" | ||||
|         return (dt_util.utcnow() - self.last_seen) > STALE_DEVICE_TIMEOUT | ||||
| @@ -1,16 +0,0 @@ | ||||
| { | ||||
|   "domain": "actron_air", | ||||
|   "name": "Actron Air", | ||||
|   "codeowners": ["@kclif9", "@JagadishDhanamjayam"], | ||||
|   "config_flow": true, | ||||
|   "dhcp": [ | ||||
|     { | ||||
|       "hostname": "neo-*", | ||||
|       "macaddress": "FC0FE7*" | ||||
|     } | ||||
|   ], | ||||
|   "documentation": "https://www.home-assistant.io/integrations/actron_air", | ||||
|   "iot_class": "cloud_polling", | ||||
|   "quality_scale": "bronze", | ||||
|   "requirements": ["actron-neo-api==0.1.84"] | ||||
| } | ||||
| @@ -1,78 +0,0 @@ | ||||
| rules: | ||||
|   # Bronze | ||||
|   action-setup: | ||||
|     status: exempt | ||||
|     comment: This integration does not have custom service actions. | ||||
|   appropriate-polling: done | ||||
|   brands: done | ||||
|   common-modules: done | ||||
|   config-flow-test-coverage: done | ||||
|   config-flow: done | ||||
|   dependency-transparency: done | ||||
|   docs-actions: | ||||
|     status: exempt | ||||
|     comment: This integration does not have custom service actions. | ||||
|   docs-high-level-description: done | ||||
|   docs-installation-instructions: done | ||||
|   docs-removal-instructions: done | ||||
|   entity-event-setup: | ||||
|     status: exempt | ||||
|     comment: This integration does not subscribe to external 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: todo | ||||
|   config-entry-unloading: done | ||||
|   docs-configuration-parameters: | ||||
|     status: exempt | ||||
|     comment: No options flow | ||||
|   docs-installation-parameters: done | ||||
|   entity-unavailable: done | ||||
|   integration-owner: done | ||||
|   log-when-unavailable: done | ||||
|   parallel-updates: done | ||||
|   reauthentication-flow: todo | ||||
|   test-coverage: todo | ||||
|  | ||||
|   # Gold | ||||
|   devices: done | ||||
|   diagnostics: todo | ||||
|   discovery-update-info: | ||||
|     status: exempt | ||||
|     comment: This integration uses DHCP discovery, however is cloud polling. Therefore there is no information to update. | ||||
|   discovery: done | ||||
|   docs-data-update: done | ||||
|   docs-examples: done | ||||
|   docs-known-limitations: done | ||||
|   docs-supported-devices: done | ||||
|   docs-supported-functions: done | ||||
|   docs-troubleshooting: done | ||||
|   docs-use-cases: done | ||||
|   dynamic-devices: todo | ||||
|   entity-category: | ||||
|     status: exempt | ||||
|     comment: This integration does not use entity categories. | ||||
|   entity-device-class: | ||||
|     status: exempt | ||||
|     comment: This integration does not use entity device classes. | ||||
|   entity-disabled-by-default: | ||||
|     status: exempt | ||||
|     comment: Not required for this integration at this stage. | ||||
|   entity-translations: todo | ||||
|   exception-translations: todo | ||||
|   icon-translations: todo | ||||
|   reconfiguration-flow: todo | ||||
|   repair-issues: | ||||
|     status: exempt | ||||
|     comment: This integration does not have any known issues that require repair. | ||||
|   stale-devices: todo | ||||
|  | ||||
|   # Platinum | ||||
|   async-dependency: done | ||||
|   inject-websession: todo | ||||
|   strict-typing: todo | ||||
| @@ -1,29 +0,0 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", | ||||
|       "oauth2_error": "Failed to start OAuth2 flow" | ||||
|     }, | ||||
|     "error": { | ||||
|       "oauth2_error": "Failed to start OAuth2 flow. Please try again later." | ||||
|     }, | ||||
|     "progress": { | ||||
|       "wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes." | ||||
|     }, | ||||
|     "step": { | ||||
|       "connection_error": { | ||||
|         "data": {}, | ||||
|         "description": "Failed to connect to Actron Air. Please check your internet connection and try again.", | ||||
|         "title": "Connection error" | ||||
|       }, | ||||
|       "timeout": { | ||||
|         "data": {}, | ||||
|         "description": "The authorization process timed out. Please try again.", | ||||
|         "title": "Authorization timeout" | ||||
|       }, | ||||
|       "user": { | ||||
|         "title": "Actron Air OAuth2 Authorization" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -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"] | ||||
| } | ||||
|   | ||||
| @@ -1,34 +1,34 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "heater_not_available": "Heater not available. Try to reset the heater by pressing + and OK for some seconds.", | ||||
|       "heater_not_found": "Heater not found. Try to move the heater closer to Home Assistant computer.", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "cloud": { | ||||
|         "data": { | ||||
|           "account_id": "Account ID", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         } | ||||
|       }, | ||||
|       "local": { | ||||
|         "data": { | ||||
|           "wifi_pswd": "Wi-Fi password", | ||||
|           "wifi_ssid": "Wi-Fi SSID" | ||||
|         }, | ||||
|         "description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue LED starts blinking before pressing Submit. Configuring heater might take some minutes." | ||||
|       }, | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "connection_type": "Select connection type" | ||||
|         }, | ||||
|         "description": "Select connection type. Local requires heaters with Bluetooth" | ||||
|       }, | ||||
|       "local": { | ||||
|         "data": { | ||||
|           "wifi_ssid": "Wi-Fi SSID", | ||||
|           "wifi_pswd": "Wi-Fi password" | ||||
|         }, | ||||
|         "description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue LED starts blinking before pressing Submit. Configuring heater might take some minutes." | ||||
|       }, | ||||
|       "cloud": { | ||||
|         "data": { | ||||
|           "account_id": "Account ID", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "heater_not_available": "Heater not available. Try to reset the heater by pressing + and OK for some seconds.", | ||||
|       "heater_not_found": "Heater not found. Try to move the heater closer to Home Assistant computer.", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| { | ||||
|   "entity": { | ||||
|     "sensor": { | ||||
|       "average_processing_speed": { | ||||
|         "default": "mdi:speedometer" | ||||
|       }, | ||||
|       "dns_queries": { | ||||
|         "default": "mdi:magnify" | ||||
|       }, | ||||
| @@ -16,18 +13,21 @@ | ||||
|       "parental_control_blocked": { | ||||
|         "default": "mdi:human-male-girl" | ||||
|       }, | ||||
|       "rules_count": { | ||||
|         "default": "mdi:counter" | ||||
|       }, | ||||
|       "safe_browsing_blocked": { | ||||
|         "default": "mdi:shield-half-full" | ||||
|       }, | ||||
|       "safe_searches_enforced": { | ||||
|         "default": "mdi:shield-search" | ||||
|       }, | ||||
|       "average_processing_speed": { | ||||
|         "default": "mdi:speedometer" | ||||
|       }, | ||||
|       "rules_count": { | ||||
|         "default": "mdi:counter" | ||||
|       } | ||||
|     }, | ||||
|     "switch": { | ||||
|       "filtering": { | ||||
|       "protection": { | ||||
|         "default": "mdi:shield-check", | ||||
|         "state": { | ||||
|           "off": "mdi:shield-off" | ||||
| @@ -39,13 +39,7 @@ | ||||
|           "off": "mdi:shield-off" | ||||
|         } | ||||
|       }, | ||||
|       "protection": { | ||||
|         "default": "mdi:shield-check", | ||||
|         "state": { | ||||
|           "off": "mdi:shield-off" | ||||
|         } | ||||
|       }, | ||||
|       "query_log": { | ||||
|       "safe_search": { | ||||
|         "default": "mdi:shield-check", | ||||
|         "state": { | ||||
|           "off": "mdi:shield-off" | ||||
| @@ -57,7 +51,13 @@ | ||||
|           "off": "mdi:shield-off" | ||||
|         } | ||||
|       }, | ||||
|       "safe_search": { | ||||
|       "filtering": { | ||||
|         "default": "mdi:shield-check", | ||||
|         "state": { | ||||
|           "off": "mdi:shield-off" | ||||
|         } | ||||
|       }, | ||||
|       "query_log": { | ||||
|         "default": "mdi:shield-check", | ||||
|         "state": { | ||||
|           "off": "mdi:shield-off" | ||||
| @@ -69,17 +69,17 @@ | ||||
|     "add_url": { | ||||
|       "service": "mdi:link-plus" | ||||
|     }, | ||||
|     "disable_url": { | ||||
|       "service": "mdi:link-variant-off" | ||||
|     "remove_url": { | ||||
|       "service": "mdi:link-off" | ||||
|     }, | ||||
|     "enable_url": { | ||||
|       "service": "mdi:link-variant" | ||||
|     }, | ||||
|     "disable_url": { | ||||
|       "service": "mdi:link-variant-off" | ||||
|     }, | ||||
|     "refresh": { | ||||
|       "service": "mdi:refresh" | ||||
|     }, | ||||
|     "remove_url": { | ||||
|       "service": "mdi:link-off" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,38 +1,35 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", | ||||
|       "existing_instance_updated": "Updated existing configuration." | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "hassio_confirm": { | ||||
|         "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?", | ||||
|         "title": "AdGuard Home via Home Assistant add-on" | ||||
|       }, | ||||
|       "user": { | ||||
|         "description": "Set up your AdGuard Home instance to allow monitoring and control.", | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]", | ||||
|           "port": "[%key:common::config_flow::data::port%]", | ||||
|           "ssl": "[%key:common::config_flow::data::ssl%]", | ||||
|           "username": "[%key:common::config_flow::data::username%]", | ||||
|           "ssl": "[%key:common::config_flow::data::ssl%]", | ||||
|           "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "host": "The hostname or IP address of the device running your AdGuard Home." | ||||
|         }, | ||||
|         "description": "Set up your AdGuard Home instance to allow monitoring and control." | ||||
|         } | ||||
|       }, | ||||
|       "hassio_confirm": { | ||||
|         "title": "AdGuard Home via Home Assistant add-on", | ||||
|         "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?" | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "existing_instance_updated": "Updated existing configuration.", | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
|     "sensor": { | ||||
|       "average_processing_speed": { | ||||
|         "name": "Average processing speed" | ||||
|       }, | ||||
|       "dns_queries": { | ||||
|         "name": "DNS queries" | ||||
|       }, | ||||
| @@ -45,91 +42,94 @@ | ||||
|       "parental_control_blocked": { | ||||
|         "name": "Parental control blocked" | ||||
|       }, | ||||
|       "rules_count": { | ||||
|         "name": "Rules count" | ||||
|       }, | ||||
|       "safe_browsing_blocked": { | ||||
|         "name": "Safe browsing blocked" | ||||
|       }, | ||||
|       "safe_searches_enforced": { | ||||
|         "name": "Safe searches enforced" | ||||
|       }, | ||||
|       "average_processing_speed": { | ||||
|         "name": "Average processing speed" | ||||
|       }, | ||||
|       "rules_count": { | ||||
|         "name": "Rules count" | ||||
|       } | ||||
|     }, | ||||
|     "switch": { | ||||
|       "filtering": { | ||||
|         "name": "Filtering" | ||||
|       "protection": { | ||||
|         "name": "Protection" | ||||
|       }, | ||||
|       "parental": { | ||||
|         "name": "Parental control" | ||||
|       }, | ||||
|       "protection": { | ||||
|         "name": "Protection" | ||||
|       }, | ||||
|       "query_log": { | ||||
|         "name": "Query log" | ||||
|       "safe_search": { | ||||
|         "name": "Safe search" | ||||
|       }, | ||||
|       "safe_browsing": { | ||||
|         "name": "Safe browsing" | ||||
|       }, | ||||
|       "safe_search": { | ||||
|         "name": "Safe search" | ||||
|       "filtering": { | ||||
|         "name": "Filtering" | ||||
|       }, | ||||
|       "query_log": { | ||||
|         "name": "Query log" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "services": { | ||||
|     "add_url": { | ||||
|       "name": "Add URL", | ||||
|       "description": "Adds a new filter subscription to AdGuard Home.", | ||||
|       "fields": { | ||||
|         "name": { | ||||
|           "description": "The name of the filter subscription.", | ||||
|           "name": "[%key:common::config_flow::data::name%]" | ||||
|           "name": "[%key:common::config_flow::data::name%]", | ||||
|           "description": "The name of the filter subscription." | ||||
|         }, | ||||
|         "url": { | ||||
|           "description": "The filter URL to subscribe to, containing the filter rules.", | ||||
|           "name": "[%key:common::config_flow::data::url%]" | ||||
|           "name": "[%key:common::config_flow::data::url%]", | ||||
|           "description": "The filter URL to subscribe to, containing the filter rules." | ||||
|         } | ||||
|       }, | ||||
|       "name": "Add URL" | ||||
|     }, | ||||
|     "disable_url": { | ||||
|       "description": "Disables a filter subscription in AdGuard Home.", | ||||
|       "fields": { | ||||
|         "url": { | ||||
|           "description": "The filter subscription URL to disable.", | ||||
|           "name": "[%key:common::config_flow::data::url%]" | ||||
|       } | ||||
|     }, | ||||
|       "name": "Disable URL" | ||||
|     }, | ||||
|     "enable_url": { | ||||
|       "description": "Enables a filter subscription in AdGuard Home.", | ||||
|       "fields": { | ||||
|         "url": { | ||||
|           "description": "The filter subscription URL to enable.", | ||||
|           "name": "[%key:common::config_flow::data::url%]" | ||||
|         } | ||||
|       }, | ||||
|       "name": "Enable URL" | ||||
|     }, | ||||
|     "refresh": { | ||||
|       "description": "Refreshes all filter subscriptions in AdGuard Home.", | ||||
|       "fields": { | ||||
|         "force": { | ||||
|           "description": "Force update (bypasses AdGuard Home throttling), omit for a regular refresh.", | ||||
|           "name": "Force" | ||||
|         } | ||||
|       }, | ||||
|       "name": "Refresh" | ||||
|     }, | ||||
|     "remove_url": { | ||||
|       "name": "Remove URL", | ||||
|       "description": "Removes a filter subscription from AdGuard Home.", | ||||
|       "fields": { | ||||
|         "url": { | ||||
|           "description": "The filter subscription URL to remove.", | ||||
|           "name": "[%key:common::config_flow::data::url%]" | ||||
|           "name": "[%key:common::config_flow::data::url%]", | ||||
|           "description": "The filter subscription URL to remove." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|       "name": "Remove URL" | ||||
|     "enable_url": { | ||||
|       "name": "Enable URL", | ||||
|       "description": "Enables a filter subscription in AdGuard Home.", | ||||
|       "fields": { | ||||
|         "url": { | ||||
|           "name": "[%key:common::config_flow::data::url%]", | ||||
|           "description": "The filter subscription URL to enable." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "disable_url": { | ||||
|       "name": "Disable URL", | ||||
|       "description": "Disables a filter subscription in AdGuard Home.", | ||||
|       "fields": { | ||||
|         "url": { | ||||
|           "name": "[%key:common::config_flow::data::url%]", | ||||
|           "description": "The filter subscription URL to disable." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "refresh": { | ||||
|       "name": "Refresh", | ||||
|       "description": "Refreshes all filter subscriptions in AdGuard Home.", | ||||
|       "fields": { | ||||
|         "force": { | ||||
|           "name": "Force", | ||||
|           "description": "Force update (bypasses AdGuard Home throttling), omit for a regular refresh." | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,22 +1,22 @@ | ||||
| { | ||||
|   "services": { | ||||
|     "write_data_by_name": { | ||||
|       "name": "Write data by name", | ||||
|       "description": "Write a value to the connected ADS device.", | ||||
|       "fields": { | ||||
|         "adstype": { | ||||
|           "description": "The data type of the variable to write to.", | ||||
|           "name": "ADS type" | ||||
|         }, | ||||
|         "adsvar": { | ||||
|           "description": "The name of the variable to write to.", | ||||
|           "name": "ADS variable" | ||||
|           "name": "ADS variable", | ||||
|           "description": "The name of the variable to write to." | ||||
|         }, | ||||
|         "adstype": { | ||||
|           "name": "ADS type", | ||||
|           "description": "The data type of the variable to write to." | ||||
|         }, | ||||
|         "value": { | ||||
|           "description": "The value to write to the variable.", | ||||
|           "name": "Value" | ||||
|           "name": "Value", | ||||
|           "description": "The value to write to the variable." | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|       "name": "Write data by name" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
| @@ -19,14 +19,14 @@ | ||||
|   }, | ||||
|   "services": { | ||||
|     "set_time_to": { | ||||
|       "name": "Set time to", | ||||
|       "description": "Controls timers to turn the system on or off after a set number of minutes.", | ||||
|       "fields": { | ||||
|         "minutes": { | ||||
|           "description": "Minutes until action.", | ||||
|           "name": "Minutes" | ||||
|           "name": "Minutes", | ||||
|           "description": "Minutes until action." | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|       "name": "Set time to" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -71,14 +71,7 @@ class AemetConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         return self.async_show_form( | ||||
|             step_id="user", | ||||
|             data_schema=schema, | ||||
|             errors=errors, | ||||
|             description_placeholders={ | ||||
|                 "api_key_url": "https://opendata.aemet.es/centrodedescargas/altaUsuario" | ||||
|             }, | ||||
|         ) | ||||
|         return self.async_show_form(step_id="user", data_schema=schema, errors=errors) | ||||
|  | ||||
|     @staticmethod | ||||
|     @callback | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
|           "longitude": "[%key:common::config_flow::data::longitude%]", | ||||
|           "name": "Name of the integration" | ||||
|         }, | ||||
|         "description": "To generate API key go to {api_key_url}" | ||||
|         "description": "To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   | ||||
| @@ -1,57 +1,57 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     }, | ||||
|   "issues": { | ||||
|     "deprecated_yaml_import_issue_cannot_connect": { | ||||
|       "description": "Configuring {integration_title} using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually.", | ||||
|       "title": "The {integration_title} YAML configuration import failed" | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     } | ||||
|   }, | ||||
|   "services": { | ||||
|     "add_tracking": { | ||||
|       "name": "Add tracking", | ||||
|       "description": "Adds a new tracking number to Aftership.", | ||||
|       "fields": { | ||||
|         "slug": { | ||||
|           "description": "Slug (carrier) of the new tracking.", | ||||
|           "name": "Slug" | ||||
|         "tracking_number": { | ||||
|           "name": "Tracking number", | ||||
|           "description": "Tracking number for the new tracking." | ||||
|         }, | ||||
|         "title": { | ||||
|           "description": "A custom title for the new tracking.", | ||||
|           "name": "Title" | ||||
|           "name": "Title", | ||||
|           "description": "A custom title for the new tracking." | ||||
|         }, | ||||
|         "tracking_number": { | ||||
|           "description": "Tracking number for the new tracking.", | ||||
|           "name": "Tracking number" | ||||
|         "slug": { | ||||
|           "name": "Slug", | ||||
|           "description": "Slug (carrier) of the new tracking." | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|       "name": "Add tracking" | ||||
|     }, | ||||
|     "remove_tracking": { | ||||
|       "name": "Remove tracking", | ||||
|       "description": "Removes a tracking number from Aftership.", | ||||
|       "fields": { | ||||
|         "slug": { | ||||
|           "description": "Slug (carrier) of the tracking to remove.", | ||||
|           "name": "[%key:component::aftership::services::add_tracking::fields::slug::name%]" | ||||
|         }, | ||||
|         "tracking_number": { | ||||
|           "description": "Tracking number of the tracking to remove.", | ||||
|           "name": "[%key:component::aftership::services::add_tracking::fields::tracking_number::name%]" | ||||
|           "name": "[%key:component::aftership::services::add_tracking::fields::tracking_number::name%]", | ||||
|           "description": "Tracking number of the tracking to remove." | ||||
|         }, | ||||
|         "slug": { | ||||
|           "name": "[%key:component::aftership::services::add_tracking::fields::slug::name%]", | ||||
|           "description": "Slug (carrier) of the tracking to remove." | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|       "name": "Remove tracking" | ||||
|   "issues": { | ||||
|     "deprecated_yaml_import_issue_cannot_connect": { | ||||
|       "title": "The {integration_title} YAML configuration import failed", | ||||
|       "description": "Configuring {integration_title} using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,19 +1,19 @@ | ||||
| { | ||||
|   "services": { | ||||
|     "disable_alerts": { | ||||
|       "service": "mdi:bell-off" | ||||
|     }, | ||||
|     "enable_alerts": { | ||||
|       "service": "mdi:bell-alert" | ||||
|     }, | ||||
|     "snapshot": { | ||||
|       "service": "mdi:camera" | ||||
|     }, | ||||
|     "start_recording": { | ||||
|       "service": "mdi:record-rec" | ||||
|     }, | ||||
|     "stop_recording": { | ||||
|       "service": "mdi:stop" | ||||
|     }, | ||||
|     "enable_alerts": { | ||||
|       "service": "mdi:bell-alert" | ||||
|     }, | ||||
|     "disable_alerts": { | ||||
|       "service": "mdi:bell-off" | ||||
|     }, | ||||
|     "snapshot": { | ||||
|       "service": "mdi:camera" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,45 +1,45 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "title": "Set up Agent DVR", | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]", | ||||
|           "port": "[%key:common::config_flow::data::port%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "host": "The IP address of the Agent DVR server." | ||||
|         }, | ||||
|         "title": "Set up Agent DVR" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     } | ||||
|   }, | ||||
|   "services": { | ||||
|     "disable_alerts": { | ||||
|       "description": "Disables alerts.", | ||||
|       "name": "Disable alerts" | ||||
|     }, | ||||
|     "enable_alerts": { | ||||
|       "description": "Enables alerts.", | ||||
|       "name": "Enable alerts" | ||||
|     }, | ||||
|     "snapshot": { | ||||
|       "description": "Takes a photo.", | ||||
|       "name": "Snapshot" | ||||
|     }, | ||||
|     "start_recording": { | ||||
|       "description": "Enables continuous recording.", | ||||
|       "name": "Start recording" | ||||
|       "name": "Start recording", | ||||
|       "description": "Enables continuous recording." | ||||
|     }, | ||||
|     "stop_recording": { | ||||
|       "description": "Disables continuous recording.", | ||||
|       "name": "Stop recording" | ||||
|       "name": "Stop recording", | ||||
|       "description": "Disables continuous recording." | ||||
|     }, | ||||
|     "enable_alerts": { | ||||
|       "name": "Enable alerts", | ||||
|       "description": "Enables alerts." | ||||
|     }, | ||||
|     "disable_alerts": { | ||||
|       "name": "Disable alerts", | ||||
|       "description": "Disables alerts." | ||||
|     }, | ||||
|     "snapshot": { | ||||
|       "name": "Snapshot", | ||||
|       "description": "Takes a photo." | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -53,6 +53,9 @@ __all__ = [ | ||||
|     "GenImageTaskResult", | ||||
|     "async_generate_data", | ||||
|     "async_generate_image", | ||||
|     "async_setup", | ||||
|     "async_setup_entry", | ||||
|     "async_unload_entry", | ||||
| ] | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|   | ||||
| @@ -1,52 +1,52 @@ | ||||
| { | ||||
|   "services": { | ||||
|     "generate_data": { | ||||
|       "name": "Generate data", | ||||
|       "description": "Uses AI to run a task that generates data.", | ||||
|       "fields": { | ||||
|         "attachments": { | ||||
|           "description": "List of files to attach for multi-modal AI analysis.", | ||||
|           "name": "Attachments" | ||||
|         }, | ||||
|         "entity_id": { | ||||
|           "description": "Entity ID to run the task on. If not provided, the preferred entity will be used.", | ||||
|           "name": "Entity ID" | ||||
|         "task_name": { | ||||
|           "name": "Task name", | ||||
|           "description": "Name of the task." | ||||
|         }, | ||||
|         "instructions": { | ||||
|           "description": "Instructions on what needs to be done.", | ||||
|           "name": "Instructions" | ||||
|           "name": "Instructions", | ||||
|           "description": "Instructions on what needs to be done." | ||||
|         }, | ||||
|         "entity_id": { | ||||
|           "name": "Entity ID", | ||||
|           "description": "Entity ID to run the task on. If not provided, the preferred entity will be used." | ||||
|         }, | ||||
|         "structure": { | ||||
|           "description": "When set, the AI Task will output fields with this in structure. The structure is a dictionary where the keys are the field names and the values contain a 'description', a 'selector', and an optional 'required' field.", | ||||
|           "name": "Structured output" | ||||
|           "name": "Structured output", | ||||
|           "description": "When set, the AI Task will output fields with this in structure. The structure is a dictionary where the keys are the field names and the values contain a 'description', a 'selector', and an optional 'required' field." | ||||
|         }, | ||||
|         "task_name": { | ||||
|           "description": "Name of the task.", | ||||
|           "name": "Task name" | ||||
|         "attachments": { | ||||
|           "name": "Attachments", | ||||
|           "description": "List of files to attach for multi-modal AI analysis." | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|       "name": "Generate data" | ||||
|     }, | ||||
|     "generate_image": { | ||||
|       "name": "Generate image", | ||||
|       "description": "Uses AI to generate image.", | ||||
|       "fields": { | ||||
|         "attachments": { | ||||
|           "description": "List of files to attach for using as references.", | ||||
|           "name": "Attachments" | ||||
|         }, | ||||
|         "entity_id": { | ||||
|           "description": "Entity ID to run the task on.", | ||||
|           "name": "Entity ID" | ||||
|         "task_name": { | ||||
|           "name": "Task name", | ||||
|           "description": "Name of the task." | ||||
|         }, | ||||
|         "instructions": { | ||||
|           "description": "Instructions that explains the image to be generated.", | ||||
|           "name": "Instructions" | ||||
|           "name": "Instructions", | ||||
|           "description": "Instructions that explains the image to be generated." | ||||
|         }, | ||||
|         "task_name": { | ||||
|           "description": "Name of the task.", | ||||
|           "name": "Task name" | ||||
|         "entity_id": { | ||||
|           "name": "Entity ID", | ||||
|           "description": "Entity ID to run the task on." | ||||
|         }, | ||||
|         "attachments": { | ||||
|           "name": "Attachments", | ||||
|           "description": "List of files to attach for using as references." | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|       "name": "Generate image" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -9,17 +9,14 @@ | ||||
|       } | ||||
|     }, | ||||
|     "number": { | ||||
|       "display_brightness": { | ||||
|       "led_bar_brightness": { | ||||
|         "default": "mdi:brightness-percent" | ||||
|       }, | ||||
|       "led_bar_brightness": { | ||||
|       "display_brightness": { | ||||
|         "default": "mdi:brightness-percent" | ||||
|       } | ||||
|     }, | ||||
|     "select": { | ||||
|       "co2_automatic_baseline_calibration": { | ||||
|         "default": "mdi:molecule-co2" | ||||
|       }, | ||||
|       "configuration_control": { | ||||
|         "default": "mdi:cloud-cog" | ||||
|       }, | ||||
| @@ -34,11 +31,23 @@ | ||||
|       }, | ||||
|       "voc_index_learning_time_offset": { | ||||
|         "default": "mdi:clock-outline" | ||||
|       }, | ||||
|       "co2_automatic_baseline_calibration": { | ||||
|         "default": "mdi:molecule-co2" | ||||
|       } | ||||
|     }, | ||||
|     "sensor": { | ||||
|       "co2_automatic_baseline_calibration": { | ||||
|         "default": "mdi:molecule-co2" | ||||
|       "total_volatile_organic_component_index": { | ||||
|         "default": "mdi:molecule" | ||||
|       }, | ||||
|       "nitrogen_index": { | ||||
|         "default": "mdi:molecule" | ||||
|       }, | ||||
|       "pm003_count": { | ||||
|         "default": "mdi:blur" | ||||
|       }, | ||||
|       "led_bar_brightness": { | ||||
|         "default": "mdi:brightness-percent" | ||||
|       }, | ||||
|       "display_brightness": { | ||||
|         "default": "mdi:brightness-percent" | ||||
| @@ -46,26 +55,17 @@ | ||||
|       "display_temperature_unit": { | ||||
|         "default": "mdi:thermometer-lines" | ||||
|       }, | ||||
|       "led_bar_brightness": { | ||||
|         "default": "mdi:brightness-percent" | ||||
|       }, | ||||
|       "led_bar_mode": { | ||||
|         "default": "mdi:led-strip" | ||||
|       }, | ||||
|       "nitrogen_index": { | ||||
|         "default": "mdi:molecule" | ||||
|       }, | ||||
|       "nox_index_learning_time_offset": { | ||||
|         "default": "mdi:clock-outline" | ||||
|       }, | ||||
|       "pm003_count": { | ||||
|         "default": "mdi:blur" | ||||
|       }, | ||||
|       "total_volatile_organic_component_index": { | ||||
|         "default": "mdi:molecule" | ||||
|       }, | ||||
|       "voc_index_learning_time_offset": { | ||||
|         "default": "mdi:clock-outline" | ||||
|       }, | ||||
|       "co2_automatic_baseline_calibration": { | ||||
|         "default": "mdi:molecule-co2" | ||||
|       } | ||||
|     }, | ||||
|     "switch": { | ||||
|   | ||||
| @@ -1,5 +1,19 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "flow_title": "{model}", | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "host": "The hostname or IP address of the Airgradient device." | ||||
|         } | ||||
|       }, | ||||
|       "discovery_confirm": { | ||||
|         "description": "Do you want to set up {model}?" | ||||
|       } | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||||
| @@ -10,20 +24,6 @@ | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "flow_title": "{model}", | ||||
|     "step": { | ||||
|       "discovery_confirm": { | ||||
|         "description": "Do you want to set up {model}?" | ||||
|       }, | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "host": "The hostname or IP address of the Airgradient device." | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
| @@ -36,25 +36,14 @@ | ||||
|       } | ||||
|     }, | ||||
|     "number": { | ||||
|       "display_brightness": { | ||||
|         "name": "Display brightness" | ||||
|       }, | ||||
|       "led_bar_brightness": { | ||||
|         "name": "LED bar brightness" | ||||
|       }, | ||||
|       "display_brightness": { | ||||
|         "name": "Display brightness" | ||||
|       } | ||||
|     }, | ||||
|     "select": { | ||||
|       "co2_automatic_baseline_calibration": { | ||||
|         "name": "CO2 automatic baseline duration", | ||||
|         "state": { | ||||
|           "0": "[%key:common::state::off%]", | ||||
|           "1": "1 day", | ||||
|           "8": "8 days", | ||||
|           "30": "30 days", | ||||
|           "90": "90 days", | ||||
|           "180": "180 days" | ||||
|         } | ||||
|       }, | ||||
|       "configuration_control": { | ||||
|         "name": "Configuration source", | ||||
|         "state": { | ||||
| @@ -62,13 +51,6 @@ | ||||
|           "local": "Local" | ||||
|         } | ||||
|       }, | ||||
|       "display_pm_standard": { | ||||
|         "name": "Display PM standard", | ||||
|         "state": { | ||||
|           "ugm3": "μg/m³", | ||||
|           "us_aqi": "US AQI" | ||||
|         } | ||||
|       }, | ||||
|       "display_temperature_unit": { | ||||
|         "name": "Display temperature unit", | ||||
|         "state": { | ||||
| @@ -76,11 +58,18 @@ | ||||
|           "f": "Fahrenheit" | ||||
|         } | ||||
|       }, | ||||
|       "display_pm_standard": { | ||||
|         "name": "Display PM standard", | ||||
|         "state": { | ||||
|           "ugm3": "μg/m³", | ||||
|           "us_aqi": "US AQI" | ||||
|         } | ||||
|       }, | ||||
|       "led_bar_mode": { | ||||
|         "name": "LED bar mode", | ||||
|         "state": { | ||||
|           "co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", | ||||
|           "off": "[%key:common::state::off%]", | ||||
|           "co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", | ||||
|           "pm": "Particulate matter" | ||||
|         } | ||||
|       }, | ||||
| @@ -103,14 +92,37 @@ | ||||
|           "360": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::360%]", | ||||
|           "720": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::720%]" | ||||
|         } | ||||
|       }, | ||||
|       "co2_automatic_baseline_calibration": { | ||||
|         "name": "CO2 automatic baseline duration", | ||||
|         "state": { | ||||
|           "1": "1 day", | ||||
|           "8": "8 days", | ||||
|           "30": "30 days", | ||||
|           "90": "90 days", | ||||
|           "180": "180 days", | ||||
|           "0": "[%key:common::state::off%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "sensor": { | ||||
|       "co2_automatic_baseline_calibration_days": { | ||||
|         "name": "Carbon dioxide automatic baseline calibration" | ||||
|       "total_volatile_organic_component_index": { | ||||
|         "name": "VOC index" | ||||
|       }, | ||||
|       "display_brightness": { | ||||
|         "name": "[%key:component::airgradient::entity::number::display_brightness::name%]" | ||||
|       "nitrogen_index": { | ||||
|         "name": "NOx index" | ||||
|       }, | ||||
|       "pm003_count": { | ||||
|         "name": "PM0.3" | ||||
|       }, | ||||
|       "raw_total_volatile_organic_component": { | ||||
|         "name": "Raw VOC" | ||||
|       }, | ||||
|       "raw_nitrogen": { | ||||
|         "name": "Raw NOx" | ||||
|       }, | ||||
|       "raw_pm02": { | ||||
|         "name": "Raw PM2.5" | ||||
|       }, | ||||
|       "display_pm_standard": { | ||||
|         "name": "[%key:component::airgradient::entity::select::display_pm_standard::name%]", | ||||
| @@ -119,6 +131,26 @@ | ||||
|           "us_aqi": "[%key:component::airgradient::entity::select::display_pm_standard::state::us_aqi%]" | ||||
|         } | ||||
|       }, | ||||
|       "co2_automatic_baseline_calibration_days": { | ||||
|         "name": "Carbon dioxide automatic baseline calibration" | ||||
|       }, | ||||
|       "nox_learning_offset": { | ||||
|         "name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]" | ||||
|       }, | ||||
|       "tvoc_learning_offset": { | ||||
|         "name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]" | ||||
|       }, | ||||
|       "led_bar_mode": { | ||||
|         "name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]", | ||||
|         "state": { | ||||
|           "off": "[%key:common::state::off%]", | ||||
|           "co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", | ||||
|           "pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]" | ||||
|         } | ||||
|       }, | ||||
|       "led_bar_brightness": { | ||||
|         "name": "[%key:component::airgradient::entity::number::led_bar_brightness::name%]" | ||||
|       }, | ||||
|       "display_temperature_unit": { | ||||
|         "name": "[%key:component::airgradient::entity::select::display_temperature_unit::name%]", | ||||
|         "state": { | ||||
| @@ -126,40 +158,8 @@ | ||||
|           "f": "[%key:component::airgradient::entity::select::display_temperature_unit::state::f%]" | ||||
|         } | ||||
|       }, | ||||
|       "led_bar_brightness": { | ||||
|         "name": "[%key:component::airgradient::entity::number::led_bar_brightness::name%]" | ||||
|       }, | ||||
|       "led_bar_mode": { | ||||
|         "name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]", | ||||
|         "state": { | ||||
|           "co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", | ||||
|           "off": "[%key:common::state::off%]", | ||||
|           "pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]" | ||||
|         } | ||||
|       }, | ||||
|       "nitrogen_index": { | ||||
|         "name": "NOx index" | ||||
|       }, | ||||
|       "nox_learning_offset": { | ||||
|         "name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]" | ||||
|       }, | ||||
|       "pm003_count": { | ||||
|         "name": "PM0.3" | ||||
|       }, | ||||
|       "raw_nitrogen": { | ||||
|         "name": "Raw NOx" | ||||
|       }, | ||||
|       "raw_pm02": { | ||||
|         "name": "Raw PM2.5" | ||||
|       }, | ||||
|       "raw_total_volatile_organic_component": { | ||||
|         "name": "Raw VOC" | ||||
|       }, | ||||
|       "total_volatile_organic_component_index": { | ||||
|         "name": "VOC index" | ||||
|       }, | ||||
|       "tvoc_learning_offset": { | ||||
|         "name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]" | ||||
|       "display_brightness": { | ||||
|         "name": "[%key:component::airgradient::entity::number::display_brightness::name%]" | ||||
|       } | ||||
|     }, | ||||
|     "switch": { | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| """Airgradient Update platform.""" | ||||
|  | ||||
| from datetime import timedelta | ||||
| import logging | ||||
|  | ||||
| from airgradient import AirGradientConnectionError | ||||
| from propcache.api import cached_property | ||||
|  | ||||
| from homeassistant.components.update import UpdateDeviceClass, UpdateEntity | ||||
| @@ -15,7 +13,6 @@ from .entity import AirGradientEntity | ||||
|  | ||||
| PARALLEL_UPDATES = 1 | ||||
| SCAN_INTERVAL = timedelta(hours=1) | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| async def async_setup_entry( | ||||
| @@ -34,7 +31,6 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity): | ||||
|     """Representation of Airgradient Update.""" | ||||
|  | ||||
|     _attr_device_class = UpdateDeviceClass.FIRMWARE | ||||
|     _server_unreachable_logged = False | ||||
|  | ||||
|     def __init__(self, coordinator: AirGradientCoordinator) -> None: | ||||
|         """Initialize the entity.""" | ||||
| @@ -51,27 +47,10 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity): | ||||
|         """Return the installed version of the entity.""" | ||||
|         return self.coordinator.data.measures.firmware_version | ||||
|  | ||||
|     @property | ||||
|     def available(self) -> bool: | ||||
|         """Return if entity is available.""" | ||||
|         return super().available and self._attr_available | ||||
|  | ||||
|     async def async_update(self) -> None: | ||||
|         """Update the entity.""" | ||||
|         try: | ||||
|         self._attr_latest_version = ( | ||||
|             await self.coordinator.client.get_latest_firmware_version( | ||||
|                 self.coordinator.serial_number | ||||
|             ) | ||||
|         ) | ||||
|         except AirGradientConnectionError: | ||||
|             self._attr_latest_version = None | ||||
|             self._attr_available = False | ||||
|             if not self._server_unreachable_logged: | ||||
|                 _LOGGER.error( | ||||
|                     "Unable to connect to AirGradient server to check for updates" | ||||
|                 ) | ||||
|                 self._server_unreachable_logged = True | ||||
|         else: | ||||
|             self._server_unreachable_logged = False | ||||
|             self._attr_available = True | ||||
|   | ||||
| @@ -18,10 +18,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||||
|  | ||||
| from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS | ||||
|  | ||||
| DESCRIPTION_PLACEHOLDERS = { | ||||
|     "developer_registration_url": "https://developer.airly.eu/register", | ||||
| } | ||||
|  | ||||
|  | ||||
| class AirlyFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|     """Config flow for Airly.""" | ||||
| @@ -89,7 +85,6 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN): | ||||
|                 } | ||||
|             ), | ||||
|             errors=errors, | ||||
|             description_placeholders=DESCRIPTION_PLACEHOLDERS, | ||||
|         ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,23 +1,30 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "description": "To generate API key go to https://developer.airly.eu/register", | ||||
|         "data": { | ||||
|           "name": "[%key:common::config_flow::data::name%]", | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]", | ||||
|           "latitude": "[%key:common::config_flow::data::latitude%]", | ||||
|           "longitude": "[%key:common::config_flow::data::longitude%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "wrong_location": "No Airly measuring stations in this area.", | ||||
|       "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", | ||||
|       "wrong_location": "[%key:component::airly::config::error::wrong_location%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", | ||||
|       "wrong_location": "No Airly measuring stations in this area." | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]", | ||||
|           "latitude": "[%key:common::config_flow::data::latitude%]", | ||||
|           "longitude": "[%key:common::config_flow::data::longitude%]", | ||||
|           "name": "[%key:common::config_flow::data::name%]" | ||||
|         }, | ||||
|         "description": "To generate API key go to {developer_registration_url}" | ||||
|     } | ||||
|   }, | ||||
|   "system_health": { | ||||
|     "info": { | ||||
|       "can_reach_server": "Reach Airly server", | ||||
|       "requests_remaining": "Remaining allowed requests", | ||||
|       "requests_per_day": "Allowed requests per day" | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
| @@ -31,18 +38,11 @@ | ||||
|     } | ||||
|   }, | ||||
|   "exceptions": { | ||||
|     "no_station": { | ||||
|       "message": "An error occurred while retrieving data from the Airly API for {entry}: no measuring stations in this area" | ||||
|     }, | ||||
|     "update_error": { | ||||
|       "message": "An error occurred while retrieving data from the Airly API for {entry}: {error}" | ||||
|     } | ||||
|     }, | ||||
|   "system_health": { | ||||
|     "info": { | ||||
|       "can_reach_server": "Reach Airly server", | ||||
|       "requests_per_day": "Allowed requests per day", | ||||
|       "requests_remaining": "Remaining allowed requests" | ||||
|     "no_station": { | ||||
|       "message": "An error occurred while retrieving data from the Airly API for {entry}: no measuring stations in this area" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -4,15 +4,15 @@ | ||||
|       "aqi": { | ||||
|         "default": "mdi:blur" | ||||
|       }, | ||||
|       "o3": { | ||||
|         "default": "mdi:blur" | ||||
|       }, | ||||
|       "pm10": { | ||||
|         "default": "mdi:blur" | ||||
|       }, | ||||
|       "pm25": { | ||||
|         "default": "mdi:blur" | ||||
|       }, | ||||
|       "o3": { | ||||
|         "default": "mdi:blur" | ||||
|       }, | ||||
|       "station": { | ||||
|         "default": "mdi:blur" | ||||
|       } | ||||
|   | ||||
| @@ -1,7 +1,15 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "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%]", | ||||
|           "longitude": "[%key:common::config_flow::data::longitude%]", | ||||
|           "radius": "Station radius (miles; optional)" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
| @@ -9,15 +17,16 @@ | ||||
|       "invalid_location": "No results found for that location, try changing the location or station radius.", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]", | ||||
|           "latitude": "[%key:common::config_flow::data::latitude%]", | ||||
|           "longitude": "[%key:common::config_flow::data::longitude%]", | ||||
|           "radius": "Station radius (miles; optional)" | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     } | ||||
|   }, | ||||
|         "description": "To generate API key go to {api_key_url}" | ||||
|   "options": { | ||||
|     "step": { | ||||
|       "init": { | ||||
|         "data": { | ||||
|           "radius": "Station radius (miles)" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @@ -34,14 +43,5 @@ | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "options": { | ||||
|     "step": { | ||||
|       "init": { | ||||
|         "data": { | ||||
|           "radius": "Station radius (miles)" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,8 +2,6 @@ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import logging | ||||
|  | ||||
| from airos.airos8 import AirOS8 | ||||
|  | ||||
| from homeassistant.const import ( | ||||
| @@ -14,11 +12,10 @@ from homeassistant.const import ( | ||||
|     CONF_VERIFY_SSL, | ||||
|     Platform, | ||||
| ) | ||||
| from homeassistant.core import HomeAssistant, callback | ||||
| from homeassistant.helpers import device_registry as dr, entity_registry as er | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||||
|  | ||||
| from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS | ||||
| from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, SECTION_ADVANCED_SETTINGS | ||||
| from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator | ||||
|  | ||||
| _PLATFORMS: list[Platform] = [ | ||||
| @@ -26,8 +23,6 @@ _PLATFORMS: list[Platform] = [ | ||||
|     Platform.SENSOR, | ||||
| ] | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: | ||||
|     """Set up Ubiquiti airOS from a config entry.""" | ||||
| @@ -59,13 +54,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo | ||||
| async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: | ||||
|     """Migrate old config entry.""" | ||||
|  | ||||
|     if entry.version > 1: | ||||
|         # This means the user has downgraded from a future version | ||||
|     if entry.version > 2: | ||||
|         return False | ||||
|  | ||||
|     # 1.1 Migrate config_entry to add advanced ssl settings | ||||
|     if entry.version == 1 and entry.minor_version == 1: | ||||
|         new_minor_version = 2 | ||||
|         new_data = {**entry.data} | ||||
|         advanced_data = { | ||||
|             CONF_SSL: DEFAULT_SSL, | ||||
| @@ -76,52 +69,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> b | ||||
|         hass.config_entries.async_update_entry( | ||||
|             entry, | ||||
|             data=new_data, | ||||
|             minor_version=new_minor_version, | ||||
|         ) | ||||
|  | ||||
|     # 2.1 Migrate binary_sensor entity unique_id from device_id to mac_address | ||||
|     #     Step 1 - migrate binary_sensor entity unique_id | ||||
|     #     Step 2 - migrate device entity identifier | ||||
|     if entry.version == 1: | ||||
|         new_version = 2 | ||||
|         new_minor_version = 1 | ||||
|  | ||||
|         mac_adress = dr.format_mac(entry.unique_id) | ||||
|  | ||||
|         device_registry = dr.async_get(hass) | ||||
|         if device_entry := device_registry.async_get_device( | ||||
|             connections={(dr.CONNECTION_NETWORK_MAC, mac_adress)} | ||||
|         ): | ||||
|             old_device_id = next( | ||||
|                 ( | ||||
|                     device_id | ||||
|                     for domain, device_id in device_entry.identifiers | ||||
|                     if domain == DOMAIN | ||||
|                 ), | ||||
|             ) | ||||
|  | ||||
|             @callback | ||||
|             def update_unique_id( | ||||
|                 entity_entry: er.RegistryEntry, | ||||
|             ) -> dict[str, str] | None: | ||||
|                 """Update unique id from device_id to mac address.""" | ||||
|                 if old_device_id and entity_entry.unique_id.startswith(old_device_id): | ||||
|                     suffix = entity_entry.unique_id.removeprefix(old_device_id) | ||||
|                     new_unique_id = f"{mac_adress}{suffix}" | ||||
|                     return {"new_unique_id": new_unique_id} | ||||
|                 return None | ||||
|  | ||||
|             await er.async_migrate_entries(hass, entry.entry_id, update_unique_id) | ||||
|  | ||||
|             new_identifiers = device_entry.identifiers.copy() | ||||
|             new_identifiers.discard((DOMAIN, old_device_id)) | ||||
|             new_identifiers.add((DOMAIN, mac_adress)) | ||||
|             device_registry.async_update_device( | ||||
|                 device_entry.id, new_identifiers=new_identifiers | ||||
|             ) | ||||
|  | ||||
|         hass.config_entries.async_update_entry( | ||||
|             entry, version=new_version, minor_version=new_minor_version | ||||
|             minor_version=2, | ||||
|         ) | ||||
|  | ||||
|     return True | ||||
|   | ||||
| @@ -98,7 +98,7 @@ class AirOSBinarySensor(AirOSEntity, BinarySensorEntity): | ||||
|         super().__init__(coordinator) | ||||
|  | ||||
|         self.entity_description = description | ||||
|         self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}" | ||||
|         self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}" | ||||
|  | ||||
|     @property | ||||
|     def is_on(self) -> bool: | ||||
|   | ||||
| @@ -15,12 +15,7 @@ from airos.exceptions import ( | ||||
| ) | ||||
| import voluptuous as vol | ||||
|  | ||||
| from homeassistant.config_entries import ( | ||||
|     SOURCE_REAUTH, | ||||
|     SOURCE_RECONFIGURE, | ||||
|     ConfigFlow, | ||||
|     ConfigFlowResult, | ||||
| ) | ||||
| from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult | ||||
| from homeassistant.const import ( | ||||
|     CONF_HOST, | ||||
|     CONF_PASSWORD, | ||||
| @@ -62,8 +57,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema( | ||||
| class AirOSConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|     """Handle a config flow for Ubiquiti airOS.""" | ||||
|  | ||||
|     VERSION = 2 | ||||
|     MINOR_VERSION = 1 | ||||
|     VERSION = 1 | ||||
|     MINOR_VERSION = 2 | ||||
|  | ||||
|     def __init__(self) -> None: | ||||
|         """Initialize the config flow.""" | ||||
| @@ -124,7 +119,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|         else: | ||||
|             await self.async_set_unique_id(airos_data.derived.mac) | ||||
|  | ||||
|             if self.source in [SOURCE_REAUTH, SOURCE_RECONFIGURE]: | ||||
|             if self.source == SOURCE_REAUTH: | ||||
|                 self._abort_if_unique_id_mismatch() | ||||
|             else: | ||||
|                 self._abort_if_unique_id_configured() | ||||
| @@ -169,54 +164,3 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|             ), | ||||
|             errors=self.errors, | ||||
|         ) | ||||
|  | ||||
|     async def async_step_reconfigure( | ||||
|         self, | ||||
|         user_input: Mapping[str, Any] | None = None, | ||||
|     ) -> ConfigFlowResult: | ||||
|         """Handle reconfiguration of airOS.""" | ||||
|         self.errors = {} | ||||
|         entry = self._get_reconfigure_entry() | ||||
|         current_data = entry.data | ||||
|  | ||||
|         if user_input is not None: | ||||
|             validate_data = {**current_data, **user_input} | ||||
|             if await self._validate_and_get_device_info(config_data=validate_data): | ||||
|                 return self.async_update_reload_and_abort( | ||||
|                     entry, | ||||
|                     data_updates=validate_data, | ||||
|                 ) | ||||
|  | ||||
|         return self.async_show_form( | ||||
|             step_id="reconfigure", | ||||
|             data_schema=vol.Schema( | ||||
|                 { | ||||
|                     vol.Required(CONF_PASSWORD): TextSelector( | ||||
|                         TextSelectorConfig( | ||||
|                             type=TextSelectorType.PASSWORD, | ||||
|                             autocomplete="current-password", | ||||
|                         ) | ||||
|                     ), | ||||
|                     vol.Required(SECTION_ADVANCED_SETTINGS): section( | ||||
|                         vol.Schema( | ||||
|                             { | ||||
|                                 vol.Required( | ||||
|                                     CONF_SSL, | ||||
|                                     default=current_data[SECTION_ADVANCED_SETTINGS][ | ||||
|                                         CONF_SSL | ||||
|                                     ], | ||||
|                                 ): bool, | ||||
|                                 vol.Required( | ||||
|                                     CONF_VERIFY_SSL, | ||||
|                                     default=current_data[SECTION_ADVANCED_SETTINGS][ | ||||
|                                         CONF_VERIFY_SSL | ||||
|                                     ], | ||||
|                                 ): bool, | ||||
|                             } | ||||
|                         ), | ||||
|                         {"collapsed": True}, | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|             errors=self.errors, | ||||
|         ) | ||||
|   | ||||
| @@ -33,14 +33,9 @@ class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]): | ||||
|         self._attr_device_info = DeviceInfo( | ||||
|             connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)}, | ||||
|             configuration_url=configuration_url, | ||||
|             identifiers={(DOMAIN, airos_data.derived.mac)}, | ||||
|             identifiers={(DOMAIN, str(airos_data.host.device_id))}, | ||||
|             manufacturer=MANUFACTURER, | ||||
|             model=airos_data.host.devmodel, | ||||
|             model_id=( | ||||
|                 sku | ||||
|                 if (sku := airos_data.derived.sku) not in ["UNKNOWN", "AMBIGUOUS"] | ||||
|                 else None | ||||
|             ), | ||||
|             name=airos_data.host.hostname, | ||||
|             sw_version=airos_data.host.fwversion, | ||||
|         ) | ||||
|   | ||||
| @@ -4,8 +4,7 @@ | ||||
|   "codeowners": ["@CoMPaTech"], | ||||
|   "config_flow": true, | ||||
|   "documentation": "https://www.home-assistant.io/integrations/airos", | ||||
|   "integration_type": "device", | ||||
|   "iot_class": "local_polling", | ||||
|   "quality_scale": "silver", | ||||
|   "requirements": ["airos==0.6.0"] | ||||
|   "quality_scale": "bronze", | ||||
|   "requirements": ["airos==0.5.4"] | ||||
| } | ||||
|   | ||||
| @@ -32,11 +32,11 @@ rules: | ||||
|   config-entry-unloading: done | ||||
|   docs-configuration-parameters: done | ||||
|   docs-installation-parameters: done | ||||
|   entity-unavailable: done | ||||
|   entity-unavailable: todo | ||||
|   integration-owner: done | ||||
|   log-when-unavailable: done | ||||
|   parallel-updates: done | ||||
|   reauthentication-flow: done | ||||
|   log-when-unavailable: todo | ||||
|   parallel-updates: todo | ||||
|   reauthentication-flow: todo | ||||
|   test-coverage: done | ||||
|  | ||||
|   # Gold | ||||
| @@ -48,9 +48,9 @@ rules: | ||||
|   docs-examples: todo | ||||
|   docs-known-limitations: done | ||||
|   docs-supported-devices: done | ||||
|   docs-supported-functions: done | ||||
|   docs-supported-functions: todo | ||||
|   docs-troubleshooting: done | ||||
|   docs-use-cases: done | ||||
|   docs-use-cases: todo | ||||
|   dynamic-devices: todo | ||||
|   entity-category: done | ||||
|   entity-device-class: done | ||||
| @@ -60,7 +60,7 @@ rules: | ||||
|   icon-translations: | ||||
|     status: exempt | ||||
|     comment: no (custom) icons used or envisioned | ||||
|   reconfiguration-flow: done | ||||
|   reconfiguration-flow: todo | ||||
|   repair-issues: todo | ||||
|   stale-devices: todo | ||||
|  | ||||
|   | ||||
| @@ -1,17 +1,5 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", | ||||
|       "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", | ||||
|       "unique_id_mismatch": "Re-authentication should be used for the same device not a new one" | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "key_data_missing": "Expected data not returned from the device, check the documentation for supported devices", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "flow_title": "Ubiquiti airOS device", | ||||
|     "step": { | ||||
|       "reauth_confirm": { | ||||
| @@ -22,37 +10,16 @@ | ||||
|           "password": "[%key:component::airos::config::step::user::data_description::password%]" | ||||
|         } | ||||
|       }, | ||||
|       "reconfigure": { | ||||
|         "data": { | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "password": "[%key:component::airos::config::step::user::data_description::password%]" | ||||
|         }, | ||||
|         "sections": { | ||||
|           "advanced_settings": { | ||||
|             "data": { | ||||
|               "ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data::ssl%]", | ||||
|               "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" | ||||
|             }, | ||||
|             "data_description": { | ||||
|               "ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::ssl%]", | ||||
|               "verify_ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::verify_ssl%]" | ||||
|             }, | ||||
|             "name": "[%key:component::airos::config::step::user::sections::advanced_settings::name%]" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]", | ||||
|           "username": "[%key:common::config_flow::data::username%]" | ||||
|           "username": "[%key:common::config_flow::data::username%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "host": "IP address or hostname of the airOS device", | ||||
|           "password": "Password configured through the UISP app or web interface", | ||||
|           "username": "Administrator username for the airOS device, normally 'ubnt'" | ||||
|           "username": "Administrator username for the airOS device, normally 'ubnt'", | ||||
|           "password": "Password configured through the UISP app or web interface" | ||||
|         }, | ||||
|         "sections": { | ||||
|           "advanced_settings": { | ||||
| @@ -63,17 +30,27 @@ | ||||
|             "data_description": { | ||||
|               "ssl": "Whether the connection should be encrypted (required for most devices)", | ||||
|               "verify_ssl": "Whether the certificate should be verified when using HTTPS. This should be off for self-signed certificates" | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|             "name": "Advanced settings" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "key_data_missing": "Expected data not returned from the device, check the documentation for supported devices", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", | ||||
|       "unique_id_mismatch": "Re-authentication should be used for the same device not a new one" | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
|     "binary_sensor": { | ||||
|       "dhcp6_server": { | ||||
|         "name": "DHCPv6 server" | ||||
|       "port_forwarding": { | ||||
|         "name": "Port forwarding" | ||||
|       }, | ||||
|       "dhcp_client": { | ||||
|         "name": "DHCP client" | ||||
| @@ -81,8 +58,8 @@ | ||||
|       "dhcp_server": { | ||||
|         "name": "DHCP server" | ||||
|       }, | ||||
|       "port_forwarding": { | ||||
|         "name": "Port forwarding" | ||||
|       "dhcp6_server": { | ||||
|         "name": "DHCPv6 server" | ||||
|       }, | ||||
|       "pppoe": { | ||||
|         "name": "PPPoE link" | ||||
| @@ -99,27 +76,20 @@ | ||||
|           "router": "Router" | ||||
|         } | ||||
|       }, | ||||
|       "host_uptime": { | ||||
|         "name": "Uptime" | ||||
|       }, | ||||
|       "wireless_antenna_gain": { | ||||
|         "name": "Antenna gain" | ||||
|       }, | ||||
|       "wireless_distance": { | ||||
|         "name": "Wireless distance" | ||||
|       "wireless_frequency": { | ||||
|         "name": "Wireless frequency" | ||||
|       }, | ||||
|       "wireless_essid": { | ||||
|         "name": "Wireless SSID" | ||||
|       }, | ||||
|       "wireless_frequency": { | ||||
|         "name": "Wireless frequency" | ||||
|       "wireless_antenna_gain": { | ||||
|         "name": "Antenna gain" | ||||
|       }, | ||||
|       "wireless_mode": { | ||||
|         "name": "Wireless mode", | ||||
|         "state": { | ||||
|           "point_to_multipoint": "Point-to-multipoint", | ||||
|           "point_to_point": "Point-to-point" | ||||
|         } | ||||
|       "wireless_throughput_tx": { | ||||
|         "name": "Throughput transmit (actual)" | ||||
|       }, | ||||
|       "wireless_throughput_rx": { | ||||
|         "name": "Throughput receive (actual)" | ||||
|       }, | ||||
|       "wireless_polling_dl_capacity": { | ||||
|         "name": "Download capacity" | ||||
| @@ -130,6 +100,12 @@ | ||||
|       "wireless_remote_hostname": { | ||||
|         "name": "Remote hostname" | ||||
|       }, | ||||
|       "host_uptime": { | ||||
|         "name": "Uptime" | ||||
|       }, | ||||
|       "wireless_distance": { | ||||
|         "name": "Wireless distance" | ||||
|       }, | ||||
|       "wireless_role": { | ||||
|         "name": "Wireless role", | ||||
|         "state": { | ||||
| @@ -137,26 +113,27 @@ | ||||
|           "station": "Station" | ||||
|         } | ||||
|       }, | ||||
|       "wireless_throughput_rx": { | ||||
|         "name": "Throughput receive (actual)" | ||||
|       }, | ||||
|       "wireless_throughput_tx": { | ||||
|         "name": "Throughput transmit (actual)" | ||||
|       "wireless_mode": { | ||||
|         "name": "Wireless mode", | ||||
|         "state": { | ||||
|           "point_to_point": "Point-to-point", | ||||
|           "point_to_multipoint": "Point-to-multipoint" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "exceptions": { | ||||
|     "cannot_connect": { | ||||
|       "message": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "error_data_missing": { | ||||
|       "message": "Data incomplete or missing" | ||||
|     }, | ||||
|     "invalid_auth": { | ||||
|       "message": "[%key:common::config_flow::error::invalid_auth%]" | ||||
|     }, | ||||
|     "cannot_connect": { | ||||
|       "message": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "key_data_missing": { | ||||
|       "message": "Key data not returned from device" | ||||
|     }, | ||||
|     "error_data_missing": { | ||||
|       "message": "Data incomplete or missing" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -7,5 +7,5 @@ | ||||
|   "integration_type": "hub", | ||||
|   "iot_class": "local_polling", | ||||
|   "loggers": ["aioairq"], | ||||
|   "requirements": ["aioairq==0.4.7"] | ||||
|   "requirements": ["aioairq==0.4.6"] | ||||
| } | ||||
|   | ||||
| @@ -1,21 +1,36 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "title": "Identify the device", | ||||
|         "description": "Provide the IP address or mDNS of the device and its password", | ||||
|         "data": { | ||||
|           "ip_address": "[%key:common::config_flow::data::ip%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "invalid_input": "[%key:common::config_flow::error::invalid_host%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "ip_address": "[%key:common::config_flow::data::ip%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     } | ||||
|   }, | ||||
|         "description": "Provide the IP address or mDNS of the device and its password", | ||||
|         "title": "Identify the device" | ||||
|   "options": { | ||||
|     "step": { | ||||
|       "init": { | ||||
|         "title": "Configure air-Q integration", | ||||
|         "data": { | ||||
|           "return_average": "Show values averaged by the device", | ||||
|           "clip_negatives": "Clip negative values" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)", | ||||
|           "clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behaviour is to clip such values to 0" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @@ -38,11 +53,8 @@ | ||||
|       "bromine": { | ||||
|         "name": "Bromine" | ||||
|       }, | ||||
|       "carbon_disulfide": { | ||||
|         "name": "Carbon disulfide" | ||||
|       }, | ||||
|       "carbon_monoxide": { | ||||
|         "name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]" | ||||
|       "methanethiol": { | ||||
|         "name": "Methanethiol" | ||||
|       }, | ||||
|       "chlorine": { | ||||
|         "name": "Chlorine" | ||||
| @@ -50,6 +62,12 @@ | ||||
|       "chlorine_dioxide": { | ||||
|         "name": "Chlorine dioxide" | ||||
|       }, | ||||
|       "carbon_disulfide": { | ||||
|         "name": "Carbon disulfide" | ||||
|       }, | ||||
|       "carbon_monoxide": { | ||||
|         "name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]" | ||||
|       }, | ||||
|       "dew_point": { | ||||
|         "name": "Dew point" | ||||
|       }, | ||||
| @@ -59,51 +77,36 @@ | ||||
|       "ethylene": { | ||||
|         "name": "Ethylene" | ||||
|       }, | ||||
|       "fluorine": { | ||||
|         "name": "Fluorine" | ||||
|       }, | ||||
|       "formaldehyde": { | ||||
|         "name": "Formaldehyde" | ||||
|       }, | ||||
|       "health_index": { | ||||
|         "name": "Health index" | ||||
|       "fluorine": { | ||||
|         "name": "Fluorine" | ||||
|       }, | ||||
|       "hydrogen_sulfide": { | ||||
|         "name": "Hydrogen sulfide" | ||||
|       }, | ||||
|       "hydrochloric_acid": { | ||||
|         "name": "Hydrochloric acid" | ||||
|       }, | ||||
|       "hydrogen": { | ||||
|         "name": "Hydrogen" | ||||
|       }, | ||||
|       "hydrogen_cyanide": { | ||||
|         "name": "Hydrogen cyanide" | ||||
|       }, | ||||
|       "hydrogen_fluoride": { | ||||
|         "name": "Hydrogen fluoride" | ||||
|       }, | ||||
|       "health_index": { | ||||
|         "name": "Health index" | ||||
|       }, | ||||
|       "hydrogen": { | ||||
|         "name": "Hydrogen" | ||||
|       }, | ||||
|       "hydrogen_peroxide": { | ||||
|         "name": "Hydrogen peroxide" | ||||
|       }, | ||||
|       "hydrogen_phosphide": { | ||||
|         "name": "Hydrogen phosphide" | ||||
|       }, | ||||
|       "hydrogen_sulfide": { | ||||
|         "name": "Hydrogen sulfide" | ||||
|       }, | ||||
|       "industrial_volatile_organic_compounds": { | ||||
|         "name": "VOCs (industrial)" | ||||
|       }, | ||||
|       "maximum_noise": { | ||||
|         "name": "Noise (maximum)" | ||||
|       }, | ||||
|       "methane": { | ||||
|         "name": "Methane" | ||||
|       }, | ||||
|       "methanethiol": { | ||||
|         "name": "Methanethiol" | ||||
|       }, | ||||
|       "noise": { | ||||
|         "name": "Noise" | ||||
|       }, | ||||
|       "organic_acid": { | ||||
|         "name": "Organic acid" | ||||
|       }, | ||||
| @@ -113,39 +116,36 @@ | ||||
|       "performance_index": { | ||||
|         "name": "Performance index" | ||||
|       }, | ||||
|       "propane": { | ||||
|         "name": "Propane" | ||||
|       }, | ||||
|       "radon": { | ||||
|         "name": "Radon" | ||||
|       }, | ||||
|       "refigerant": { | ||||
|         "name": "Refrigerant" | ||||
|       "hydrogen_phosphide": { | ||||
|         "name": "Hydrogen phosphide" | ||||
|       }, | ||||
|       "relative_pressure": { | ||||
|         "name": "Relative pressure" | ||||
|       }, | ||||
|       "propane": { | ||||
|         "name": "Propane" | ||||
|       }, | ||||
|       "refigerant": { | ||||
|         "name": "Refrigerant" | ||||
|       }, | ||||
|       "silicon_hydride": { | ||||
|         "name": "Silicon hydride" | ||||
|       }, | ||||
|       "noise": { | ||||
|         "name": "Noise" | ||||
|       }, | ||||
|       "maximum_noise": { | ||||
|         "name": "Noise (maximum)" | ||||
|       }, | ||||
|       "radon": { | ||||
|         "name": "Radon" | ||||
|       }, | ||||
|       "industrial_volatile_organic_compounds": { | ||||
|         "name": "VOCs (industrial)" | ||||
|       }, | ||||
|       "virus_index": { | ||||
|         "name": "Virus index" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "options": { | ||||
|     "step": { | ||||
|       "init": { | ||||
|         "data": { | ||||
|           "clip_negatives": "Clip negative values", | ||||
|           "return_average": "Show values averaged by the device" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behavior is to clip such values to 0", | ||||
|           "return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)" | ||||
|         }, | ||||
|         "title": "Configure air-Q integration" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,5 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
| @@ -16,21 +8,29 @@ | ||||
|         }, | ||||
|         "description": "Log in at {url} to find your credentials" | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
|     "sensor": { | ||||
|       "light": { | ||||
|         "name": "Light" | ||||
|       }, | ||||
|       "mold": { | ||||
|         "name": "Mold" | ||||
|       }, | ||||
|       "radon": { | ||||
|         "name": "Radon" | ||||
|       }, | ||||
|       "light": { | ||||
|         "name": "Light" | ||||
|       }, | ||||
|       "virus_risk": { | ||||
|         "name": "Virus Risk" | ||||
|       }, | ||||
|       "mold": { | ||||
|         "name": "Mold" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -6,13 +6,8 @@ import dataclasses | ||||
| import logging | ||||
| from typing import Any | ||||
|  | ||||
| from airthings_ble import ( | ||||
|     AirthingsBluetoothDeviceData, | ||||
|     AirthingsDevice, | ||||
|     UnsupportedDeviceError, | ||||
| ) | ||||
| from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice | ||||
| from bleak import BleakError | ||||
| from habluetooth import BluetoothServiceInfoBleak | ||||
| import voluptuous as vol | ||||
|  | ||||
| from homeassistant.components import bluetooth | ||||
| @@ -32,7 +27,6 @@ SERVICE_UUIDS = [ | ||||
|     "b42e4a8e-ade7-11e4-89d3-123b93f75cba", | ||||
|     "b42e1c08-ade7-11e4-89d3-123b93f75cba", | ||||
|     "b42e3882-ade7-11e4-89d3-123b93f75cba", | ||||
|     "b42e90a2-ade7-11e4-89d3-123b93f75cba", | ||||
| ] | ||||
|  | ||||
|  | ||||
| @@ -43,7 +37,6 @@ class Discovery: | ||||
|     name: str | ||||
|     discovery_info: BluetoothServiceInfo | ||||
|     device: AirthingsDevice | ||||
|     data: AirthingsBluetoothDeviceData | ||||
|  | ||||
|  | ||||
| def get_name(device: AirthingsDevice) -> str: | ||||
| @@ -51,7 +44,7 @@ def get_name(device: AirthingsDevice) -> str: | ||||
|  | ||||
|     name = device.friendly_name() | ||||
|     if identifier := device.identifier: | ||||
|         name += f" ({device.model.value}{identifier})" | ||||
|         name += f" ({identifier})" | ||||
|     return name | ||||
|  | ||||
|  | ||||
| @@ -69,8 +62,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|         self._discovered_device: Discovery | None = None | ||||
|         self._discovered_devices: dict[str, Discovery] = {} | ||||
|  | ||||
|     async def _get_device( | ||||
|         self, data: AirthingsBluetoothDeviceData, discovery_info: BluetoothServiceInfo | ||||
|     async def _get_device_data( | ||||
|         self, discovery_info: BluetoothServiceInfo | ||||
|     ) -> AirthingsDevice: | ||||
|         ble_device = bluetooth.async_ble_device_from_address( | ||||
|             self.hass, discovery_info.address | ||||
| @@ -79,8 +72,10 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|             _LOGGER.debug("no ble_device in _get_device_data") | ||||
|             raise AirthingsDeviceUpdateError("No ble_device") | ||||
|  | ||||
|         airthings = AirthingsBluetoothDeviceData(_LOGGER) | ||||
|  | ||||
|         try: | ||||
|             device = await data.update_device(ble_device) | ||||
|             data = await airthings.update_device(ble_device) | ||||
|         except BleakError as err: | ||||
|             _LOGGER.error( | ||||
|                 "Error connecting to and getting data from %s: %s", | ||||
| @@ -88,15 +83,12 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|                 err, | ||||
|             ) | ||||
|             raise AirthingsDeviceUpdateError("Failed getting device data") from err | ||||
|         except UnsupportedDeviceError: | ||||
|             _LOGGER.debug("Skipping unsupported device: %s", discovery_info.name) | ||||
|             raise | ||||
|         except Exception as err: | ||||
|             _LOGGER.error( | ||||
|                 "Unknown error occurred from %s: %s", discovery_info.address, err | ||||
|             ) | ||||
|             raise | ||||
|         return device | ||||
|         return data | ||||
|  | ||||
|     async def async_step_bluetooth( | ||||
|         self, discovery_info: BluetoothServiceInfo | ||||
| @@ -106,21 +98,17 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|         await self.async_set_unique_id(discovery_info.address) | ||||
|         self._abort_if_unique_id_configured() | ||||
|  | ||||
|         data = AirthingsBluetoothDeviceData(logger=_LOGGER) | ||||
|  | ||||
|         try: | ||||
|             device = await self._get_device(data=data, discovery_info=discovery_info) | ||||
|             device = await self._get_device_data(discovery_info) | ||||
|         except AirthingsDeviceUpdateError: | ||||
|             return self.async_abort(reason="cannot_connect") | ||||
|         except UnsupportedDeviceError: | ||||
|             return self.async_abort(reason="unsupported_device") | ||||
|         except Exception: | ||||
|             _LOGGER.exception("Unknown error occurred") | ||||
|             return self.async_abort(reason="unknown") | ||||
|  | ||||
|         name = get_name(device) | ||||
|         self.context["title_placeholders"] = {"name": name} | ||||
|         self._discovered_device = Discovery(name, discovery_info, device, data=data) | ||||
|         self._discovered_device = Discovery(name, discovery_info, device) | ||||
|  | ||||
|         return await self.async_step_bluetooth_confirm() | ||||
|  | ||||
| @@ -129,12 +117,6 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|     ) -> ConfigFlowResult: | ||||
|         """Confirm discovery.""" | ||||
|         if user_input is not None: | ||||
|             if ( | ||||
|                 self._discovered_device is not None | ||||
|                 and self._discovered_device.device.firmware.need_firmware_upgrade | ||||
|             ): | ||||
|                 return self.async_abort(reason="firmware_upgrade_required") | ||||
|  | ||||
|             return self.async_create_entry( | ||||
|                 title=self.context["title_placeholders"]["name"], data={} | ||||
|             ) | ||||
| @@ -155,9 +137,6 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|             self._abort_if_unique_id_configured() | ||||
|             discovery = self._discovered_devices[address] | ||||
|  | ||||
|             if discovery.device.firmware.need_firmware_upgrade: | ||||
|                 return self.async_abort(reason="firmware_upgrade_required") | ||||
|  | ||||
|             self.context["title_placeholders"] = { | ||||
|                 "name": discovery.name, | ||||
|             } | ||||
| @@ -167,47 +146,26 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): | ||||
|             return self.async_create_entry(title=discovery.name, data={}) | ||||
|  | ||||
|         current_addresses = self._async_current_ids(include_ignore=False) | ||||
|         devices: list[BluetoothServiceInfoBleak] = [] | ||||
|         for discovery_info in async_discovered_service_info(self.hass): | ||||
|             address = discovery_info.address | ||||
|             if address in current_addresses or address in self._discovered_devices: | ||||
|                 continue | ||||
|  | ||||
|             if MFCT_ID not in discovery_info.manufacturer_data: | ||||
|                 continue | ||||
|             if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids): | ||||
|                 _LOGGER.debug( | ||||
|                     "Skipping unsupported device: %s (%s)", discovery_info.name, address | ||||
|                 ) | ||||
|                 continue | ||||
|             devices.append(discovery_info) | ||||
|  | ||||
|         for discovery_info in devices: | ||||
|             address = discovery_info.address | ||||
|             data = AirthingsBluetoothDeviceData(logger=_LOGGER) | ||||
|             if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids): | ||||
|                 continue | ||||
|  | ||||
|             try: | ||||
|                 device = await self._get_device(data, discovery_info) | ||||
|                 device = await self._get_device_data(discovery_info) | ||||
|             except AirthingsDeviceUpdateError: | ||||
|                 _LOGGER.error( | ||||
|                     "Error connecting to and getting data from %s (%s)", | ||||
|                     discovery_info.name, | ||||
|                     discovery_info.address, | ||||
|                 ) | ||||
|                 continue | ||||
|             except UnsupportedDeviceError: | ||||
|                 _LOGGER.debug( | ||||
|                     "Skipping unsupported device: %s (%s)", | ||||
|                     discovery_info.name, | ||||
|                     discovery_info.address, | ||||
|                 ) | ||||
|                 continue | ||||
|                 return self.async_abort(reason="cannot_connect") | ||||
|             except Exception: | ||||
|                 _LOGGER.exception("Unknown error occurred") | ||||
|                 return self.async_abort(reason="unknown") | ||||
|             name = get_name(device) | ||||
|             _LOGGER.debug("Discovered Airthings device: %s (%s)", name, address) | ||||
|             self._discovered_devices[address] = Discovery( | ||||
|                 name, discovery_info, device, data | ||||
|             ) | ||||
|             self._discovered_devices[address] = Discovery(name, discovery_info, device) | ||||
|  | ||||
|         if not self._discovered_devices: | ||||
|             return self.async_abort(reason="no_devices_found") | ||||
|   | ||||
| @@ -4,10 +4,10 @@ | ||||
|       "radon_1day_avg": { | ||||
|         "default": "mdi:radioactive" | ||||
|       }, | ||||
|       "radon_1day_level": { | ||||
|       "radon_longterm_avg": { | ||||
|         "default": "mdi:radioactive" | ||||
|       }, | ||||
|       "radon_longterm_avg": { | ||||
|       "radon_1day_level": { | ||||
|         "default": "mdi:radioactive" | ||||
|       }, | ||||
|       "radon_longterm_level": { | ||||
|   | ||||
| @@ -17,10 +17,6 @@ | ||||
|     { | ||||
|       "manufacturer_id": 820, | ||||
|       "service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba" | ||||
|     }, | ||||
|     { | ||||
|       "manufacturer_id": 820, | ||||
|       "service_uuid": "b42e90a2-ade7-11e4-89d3-123b93f75cba" | ||||
|     } | ||||
|   ], | ||||
|   "codeowners": ["@vincegio", "@LaStrada"], | ||||
| @@ -28,5 +24,5 @@ | ||||
|   "dependencies": ["bluetooth_adapters"], | ||||
|   "documentation": "https://www.home-assistant.io/integrations/airthings_ble", | ||||
|   "iot_class": "local_polling", | ||||
|   "requirements": ["airthings-ble==1.2.0"] | ||||
|   "requirements": ["airthings-ble==0.9.2"] | ||||
| } | ||||
|   | ||||
| @@ -16,12 +16,10 @@ from homeassistant.components.sensor import ( | ||||
| from homeassistant.const import ( | ||||
|     CONCENTRATION_PARTS_PER_BILLION, | ||||
|     CONCENTRATION_PARTS_PER_MILLION, | ||||
|     LIGHT_LUX, | ||||
|     PERCENTAGE, | ||||
|     EntityCategory, | ||||
|     Platform, | ||||
|     UnitOfPressure, | ||||
|     UnitOfSoundPressure, | ||||
|     UnitOfTemperature, | ||||
| ) | ||||
| from homeassistant.core import HomeAssistant, callback | ||||
| @@ -114,21 +112,6 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = { | ||||
|         state_class=SensorStateClass.MEASUREMENT, | ||||
|         suggested_display_precision=0, | ||||
|     ), | ||||
|     "lux": SensorEntityDescription( | ||||
|         key="lux", | ||||
|         device_class=SensorDeviceClass.ILLUMINANCE, | ||||
|         native_unit_of_measurement=LIGHT_LUX, | ||||
|         state_class=SensorStateClass.MEASUREMENT, | ||||
|         suggested_display_precision=0, | ||||
|     ), | ||||
|     "noise": SensorEntityDescription( | ||||
|         key="noise", | ||||
|         translation_key="ambient_noise", | ||||
|         device_class=SensorDeviceClass.SOUND_PRESSURE, | ||||
|         native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A, | ||||
|         state_class=SensorStateClass.MEASUREMENT, | ||||
|         suggested_display_precision=0, | ||||
|     ), | ||||
| } | ||||
|  | ||||
| PARALLEL_UPDATES = 0 | ||||
|   | ||||
| @@ -1,49 +1,44 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "firmware_upgrade_required": "Your device requires a firmware upgrade. Please use the Airthings app (Android/iOS) to upgrade it.", | ||||
|       "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]", | ||||
|       "unsupported_device": "Unsupported device" | ||||
|     }, | ||||
|     "flow_title": "{name}", | ||||
|     "step": { | ||||
|       "bluetooth_confirm": { | ||||
|         "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" | ||||
|       }, | ||||
|       "user": { | ||||
|         "description": "[%key:component::bluetooth::config::step::user::description%]", | ||||
|         "data": { | ||||
|           "address": "[%key:common::config_flow::data::device%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "address": "The Airthings devices discovered via Bluetooth." | ||||
|         }, | ||||
|         "description": "[%key:component::bluetooth::config::step::user::description%]" | ||||
|         } | ||||
|       }, | ||||
|       "bluetooth_confirm": { | ||||
|         "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" | ||||
|       } | ||||
|     }, | ||||
|     "abort": { | ||||
|       "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", | ||||
|       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
|     "sensor": { | ||||
|       "ambient_noise": { | ||||
|         "name": "Ambient noise" | ||||
|       }, | ||||
|       "illuminance": { | ||||
|         "name": "[%key:component::sensor::entity_component::illuminance::name%]" | ||||
|       }, | ||||
|       "radon_1day_avg": { | ||||
|         "name": "Radon 1-day average" | ||||
|       }, | ||||
|       "radon_1day_level": { | ||||
|         "name": "Radon 1-day level" | ||||
|       }, | ||||
|       "radon_longterm_avg": { | ||||
|         "name": "Radon longterm average" | ||||
|       }, | ||||
|       "radon_1day_level": { | ||||
|         "name": "Radon 1-day level" | ||||
|       }, | ||||
|       "radon_longterm_level": { | ||||
|         "name": "Radon longterm level" | ||||
|       }, | ||||
|       "illuminance": { | ||||
|         "name": "[%key:component::sensor::entity_component::illuminance::name%]" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -9,13 +9,13 @@ | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "title": "Set up your AirTouch 4 connection details.", | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "host": "The hostname or IP address of your AirTouch controller." | ||||
|         }, | ||||
|         "title": "Set up your AirTouch 4 connection details." | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
| @@ -21,8 +21,8 @@ | ||||
|         "state_attributes": { | ||||
|           "fan_mode": { | ||||
|             "state": { | ||||
|               "intelligent_auto": "Intelligent Auto", | ||||
|               "turbo": "Turbo" | ||||
|               "turbo": "Turbo", | ||||
|               "intelligent_auto": "Intelligent Auto" | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| { | ||||
|   "entity": { | ||||
|     "sensor": { | ||||
|       "pollutant_label": { | ||||
|         "default": "mdi:chemical-weapon" | ||||
|       }, | ||||
|       "pollutant_level": { | ||||
|         "default": "mdi:gauge" | ||||
|       }, | ||||
|       "pollutant_label": { | ||||
|         "default": "mdi:chemical-weapon" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,44 +1,54 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", | ||||
|       "location_not_found": "Location not found", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "geography_by_coords": { | ||||
|         "title": "Configure a geography", | ||||
|         "description": "Use the AirVisual cloud API to monitor a latitude/longitude.", | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]", | ||||
|           "latitude": "[%key:common::config_flow::data::latitude%]", | ||||
|           "longitude": "[%key:common::config_flow::data::longitude%]" | ||||
|         }, | ||||
|         "description": "Use the AirVisual cloud API to monitor a latitude/longitude.", | ||||
|         "title": "Configure a geography" | ||||
|         } | ||||
|       }, | ||||
|       "geography_by_name": { | ||||
|         "title": "[%key:component::airvisual::config::step::geography_by_coords::title%]", | ||||
|         "description": "Use the AirVisual cloud API to monitor a city/state/country.", | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]", | ||||
|           "city": "City", | ||||
|           "country": "[%key:common::config_flow::data::country%]", | ||||
|           "state": "State" | ||||
|         }, | ||||
|         "description": "Use the AirVisual cloud API to monitor a city/state/country.", | ||||
|         "title": "[%key:component::airvisual::config::step::geography_by_coords::title%]" | ||||
|           "state": "State", | ||||
|           "country": "[%key:common::config_flow::data::country%]" | ||||
|         } | ||||
|       }, | ||||
|       "reauth_confirm": { | ||||
|         "title": "Re-authenticate AirVisual", | ||||
|         "data": { | ||||
|           "api_key": "[%key:common::config_flow::data::api_key%]" | ||||
|         }, | ||||
|         "title": "Re-authenticate AirVisual" | ||||
|         } | ||||
|       }, | ||||
|       "user": { | ||||
|         "description": "Pick what type of AirVisual data you want to monitor.", | ||||
|         "title": "Configure AirVisual" | ||||
|         "title": "Configure AirVisual", | ||||
|         "description": "Pick what type of AirVisual data you want to monitor." | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]", | ||||
|       "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", | ||||
|       "location_not_found": "Location not found", | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" | ||||
|     } | ||||
|   }, | ||||
|   "options": { | ||||
|     "step": { | ||||
|       "init": { | ||||
|         "title": "[%key:component::airvisual::config::step::user::title%]", | ||||
|         "data": { | ||||
|           "show_on_map": "Show monitored geography on the map" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @@ -57,29 +67,19 @@ | ||||
|       "pollutant_level": { | ||||
|         "state": { | ||||
|           "good": "Good", | ||||
|           "hazardous": "Hazardous", | ||||
|           "moderate": "Moderate", | ||||
|           "unhealthy": "Unhealthy", | ||||
|           "unhealthy_sensitive": "Unhealthy for sensitive groups", | ||||
|           "very_unhealthy": "Very unhealthy" | ||||
|           "very_unhealthy": "Very unhealthy", | ||||
|           "hazardous": "Hazardous" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "issues": { | ||||
|     "airvisual_pro_migration": { | ||||
|       "description": "AirVisual Pro units are now their own Home Assistant integration (as opposed to be included with the original AirVisual integration that uses the AirVisual cloud API). The Pro device located at `{ip_address}` has automatically been migrated.\n\nAs part of that migration, the Pro's device ID has changed from `{old_device_id}` to `{new_device_id}`. Please update these automations to use the new device ID: {device_automations_string}.", | ||||
|       "title": "{ip_address} is now part of the AirVisual Pro integration" | ||||
|     } | ||||
|   }, | ||||
|   "options": { | ||||
|     "step": { | ||||
|       "init": { | ||||
|         "data": { | ||||
|           "show_on_map": "Show monitored geography on the map" | ||||
|         }, | ||||
|         "title": "[%key:component::airvisual::config::step::user::title%]" | ||||
|       } | ||||
|       "title": "{ip_address} is now part of the AirVisual Pro integration", | ||||
|       "description": "AirVisual Pro units are now their own Home Assistant integration (as opposed to be included with the original AirVisual integration that uses the AirVisual cloud API). The Pro device located at `{ip_address}` has automatically been migrated.\n\nAs part of that migration, the Pro's device ID has changed from `{old_device_id}` to `{new_device_id}`. Please update these automations to use the new device ID: {device_automations_string}." | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,40 +1,40 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "reauth_confirm": { | ||||
|         "description": "[%key:component::airvisual_pro::config::step::user::description%]", | ||||
|         "data": { | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         }, | ||||
|         "description": "[%key:component::airvisual_pro::config::step::user::description%]" | ||||
|         } | ||||
|       }, | ||||
|       "user": { | ||||
|         "description": "The password can be retrieved from the AirVisual Pro's UI.", | ||||
|         "data": { | ||||
|           "ip_address": "[%key:common::config_flow::data::host%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "ip_address": "The hostname or IP address of your AirVisual Pro device." | ||||
|         }, | ||||
|         "description": "The password can be retrieved from the AirVisual Pro's UI." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
|     "sensor": { | ||||
|       "outdoor_air_quality_index": { | ||||
|         "name": "Outdoor air quality index" | ||||
|       }, | ||||
|       "pm01": { | ||||
|         "name": "PM0.1" | ||||
|       }, | ||||
|       "outdoor_air_quality_index": { | ||||
|         "name": "Outdoor air quality index" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -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"] | ||||
| } | ||||
|   | ||||
| @@ -10,15 +10,15 @@ | ||||
|     "step": { | ||||
|       "discovered_connection": { | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]", | ||||
|           "id": "[%key:component::airzone::config::step::user::data::id%]", | ||||
|           "host": "[%key:common::config_flow::data::host%]", | ||||
|           "port": "[%key:common::config_flow::data::port%]" | ||||
|         } | ||||
|       }, | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]", | ||||
|           "id": "System ID", | ||||
|           "host": "[%key:common::config_flow::data::host%]", | ||||
|           "port": "[%key:common::config_flow::data::port%]" | ||||
|         } | ||||
|       } | ||||
| @@ -37,19 +37,19 @@ | ||||
|       "grille_angles": { | ||||
|         "name": "Cold angle", | ||||
|         "state": { | ||||
|           "40deg": "40°", | ||||
|           "45deg": "45°", | ||||
|           "90deg": "90°", | ||||
|           "50deg": "50°", | ||||
|           "90deg": "90°" | ||||
|           "45deg": "45°", | ||||
|           "40deg": "40°" | ||||
|         } | ||||
|       }, | ||||
|       "heat_angles": { | ||||
|         "name": "Heat angle", | ||||
|         "state": { | ||||
|           "40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]", | ||||
|           "45deg": "[%key:component::airzone::entity::select::grille_angles::state::45deg%]", | ||||
|           "90deg": "[%key:component::airzone::entity::select::grille_angles::state::90deg%]", | ||||
|           "50deg": "[%key:component::airzone::entity::select::grille_angles::state::50deg%]", | ||||
|           "90deg": "[%key:component::airzone::entity::select::grille_angles::state::90deg%]" | ||||
|           "45deg": "[%key:component::airzone::entity::select::grille_angles::state::45deg%]", | ||||
|           "40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]" | ||||
|         } | ||||
|       }, | ||||
|       "modes": { | ||||
| @@ -66,20 +66,20 @@ | ||||
|       "q_adapt": { | ||||
|         "name": "Q-Adapt", | ||||
|         "state": { | ||||
|           "maximum": "Maximum", | ||||
|           "minimum": "Minimum", | ||||
|           "standard": "Standard", | ||||
|           "power": "Power", | ||||
|           "silence": "Silence", | ||||
|           "standard": "Standard" | ||||
|           "minimum": "Minimum", | ||||
|           "maximum": "Maximum" | ||||
|         } | ||||
|       }, | ||||
|       "sleep_times": { | ||||
|         "name": "Sleep", | ||||
|         "state": { | ||||
|           "off": "[%key:common::state::off%]", | ||||
|           "30m": "30 minutes", | ||||
|           "60m": "60 minutes", | ||||
|           "90m": "90 minutes", | ||||
|           "off": "[%key:common::state::off%]" | ||||
|           "90m": "90 minutes" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|   | ||||
| @@ -10,8 +10,8 @@ | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "id": "Installation", | ||||
|           "password": "[%key:common::config_flow::data::password%]", | ||||
|           "username": "[%key:common::config_flow::data::username%]" | ||||
|           "username": "[%key:common::config_flow::data::username%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -32,9 +32,9 @@ | ||||
|       "air_quality": { | ||||
|         "name": "Air Quality mode", | ||||
|         "state": { | ||||
|           "auto": "[%key:common::state::auto%]", | ||||
|           "off": "[%key:common::state::off%]", | ||||
|           "on": "[%key:common::state::on%]" | ||||
|           "on": "[%key:common::state::on%]", | ||||
|           "auto": "[%key:common::state::auto%]" | ||||
|         } | ||||
|       }, | ||||
|       "modes": { | ||||
|   | ||||
| @@ -1,34 +1,34 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", | ||||
|       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||||
|       "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", | ||||
|       "cloud_not_enabled": "Please make sure you run Home Assistant with `{default_config}` enabled in your configuration.yaml.", | ||||
|       "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", | ||||
|       "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", | ||||
|       "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", | ||||
|       "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", | ||||
|       "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", | ||||
|       "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", | ||||
|       "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", | ||||
|       "wrong_account": "You are authenticated with a different account than the one set up. Please authenticate with the configured account." | ||||
|     }, | ||||
|     "create_entry": { | ||||
|       "default": "[%key:common::config_flow::create_entry::authenticated%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "oauth_discovery": { | ||||
|         "description": "Home Assistant has found an Aladdin Connect device on your network. Press **Submit** to continue setting up Aladdin Connect." | ||||
|       }, | ||||
|       "pick_implementation": { | ||||
|         "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" | ||||
|       }, | ||||
|       "reauth_confirm": { | ||||
|         "description": "Aladdin Connect needs to re-authenticate your account", | ||||
|         "title": "[%key:common::config_flow::title::reauth%]" | ||||
|         "title": "[%key:common::config_flow::title::reauth%]", | ||||
|         "description": "Aladdin Connect needs to re-authenticate your account" | ||||
|       }, | ||||
|       "oauth_discovery": { | ||||
|         "description": "Home Assistant has found an Aladdin Connect device on your network. Press **Submit** to continue setting up Aladdin Connect." | ||||
|       } | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", | ||||
|       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||||
|       "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", | ||||
|       "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", | ||||
|       "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", | ||||
|       "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", | ||||
|       "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", | ||||
|       "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", | ||||
|       "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", | ||||
|       "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", | ||||
|       "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", | ||||
|       "wrong_account": "You are authenticated with a different account than the one set up. Please authenticate with the configured account.", | ||||
|       "cloud_not_enabled": "Please make sure you run Home Assistant with `{default_config}` enabled in your configuration.yaml." | ||||
|     }, | ||||
|     "create_entry": { | ||||
|       "default": "[%key:common::config_flow::create_entry::authenticated%]" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,9 +2,10 @@ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| from datetime import timedelta | ||||
| import logging | ||||
| from typing import Any, Final, final | ||||
| from typing import TYPE_CHECKING, Any, Final, final | ||||
|  | ||||
| from propcache.api import cached_property | ||||
| import voluptuous as vol | ||||
| @@ -27,6 +28,8 @@ from homeassistant.helpers import config_validation as cv | ||||
| from homeassistant.helpers.config_validation import make_entity_service_schema | ||||
| from homeassistant.helpers.entity import Entity, EntityDescription | ||||
| from homeassistant.helpers.entity_component import EntityComponent | ||||
| from homeassistant.helpers.entity_platform import EntityPlatform | ||||
| from homeassistant.helpers.frame import ReportBehavior, report_usage | ||||
| from homeassistant.helpers.typing import ConfigType | ||||
| from homeassistant.util.hass_dict import HassKey | ||||
|  | ||||
| @@ -146,11 +149,68 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A | ||||
|     ) | ||||
|     _alarm_control_panel_option_default_code: str | None = None | ||||
|  | ||||
|     __alarm_legacy_state: bool = False | ||||
|  | ||||
|     def __init_subclass__(cls, **kwargs: Any) -> None: | ||||
|         """Post initialisation processing.""" | ||||
|         super().__init_subclass__(**kwargs) | ||||
|         if any(method in cls.__dict__ for method in ("_attr_state", "state")): | ||||
|             # Integrations should use the 'alarm_state' property instead of | ||||
|             # setting the state directly. | ||||
|             cls.__alarm_legacy_state = True | ||||
|  | ||||
|     def __setattr__(self, name: str, value: Any, /) -> None: | ||||
|         """Set attribute. | ||||
|  | ||||
|         Deprecation warning if setting '_attr_state' directly | ||||
|         unless already reported. | ||||
|         """ | ||||
|         if name == "_attr_state": | ||||
|             self._report_deprecated_alarm_state_handling() | ||||
|         return super().__setattr__(name, value) | ||||
|  | ||||
|     @callback | ||||
|     def add_to_platform_start( | ||||
|         self, | ||||
|         hass: HomeAssistant, | ||||
|         platform: EntityPlatform, | ||||
|         parallel_updates: asyncio.Semaphore | None, | ||||
|     ) -> None: | ||||
|         """Start adding an entity to a platform.""" | ||||
|         super().add_to_platform_start(hass, platform, parallel_updates) | ||||
|         if self.__alarm_legacy_state: | ||||
|             self._report_deprecated_alarm_state_handling() | ||||
|  | ||||
|     @callback | ||||
|     def _report_deprecated_alarm_state_handling(self) -> None: | ||||
|         """Report on deprecated handling of alarm state. | ||||
|  | ||||
|         Integrations should implement alarm_state instead of using state directly. | ||||
|         """ | ||||
|         report_usage( | ||||
|             "is setting state directly." | ||||
|             f" Entity {self.entity_id} ({type(self)}) should implement the 'alarm_state'" | ||||
|             " property and return its state using the AlarmControlPanelState enum", | ||||
|             core_integration_behavior=ReportBehavior.ERROR, | ||||
|             custom_integration_behavior=ReportBehavior.LOG, | ||||
|             breaks_in_ha_version="2025.11", | ||||
|             integration_domain=self.platform.platform_name if self.platform else None, | ||||
|             exclude_integrations={DOMAIN}, | ||||
|         ) | ||||
|  | ||||
|     @final | ||||
|     @property | ||||
|     def state(self) -> str | None: | ||||
|         """Return the current state.""" | ||||
|         return self.alarm_state | ||||
|         if (alarm_state := self.alarm_state) is not None: | ||||
|             return alarm_state | ||||
|         if self._attr_state is not None: | ||||
|             # Backwards compatibility for integrations that set state directly | ||||
|             # Should be removed in 2025.11 | ||||
|             if TYPE_CHECKING: | ||||
|                 assert isinstance(self._attr_state, str) | ||||
|             return self._attr_state | ||||
|         return None | ||||
|  | ||||
|     @cached_property | ||||
|     def alarm_state(self) -> AlarmControlPanelState | None: | ||||
|   | ||||
| @@ -18,23 +18,23 @@ | ||||
|     "alarm_arm_away": { | ||||
|       "service": "mdi:shield-lock" | ||||
|     }, | ||||
|     "alarm_arm_custom_bypass": { | ||||
|       "service": "mdi:security" | ||||
|     }, | ||||
|     "alarm_arm_home": { | ||||
|       "service": "mdi:shield-home" | ||||
|     }, | ||||
|     "alarm_arm_night": { | ||||
|       "service": "mdi:shield-moon" | ||||
|     }, | ||||
|     "alarm_arm_vacation": { | ||||
|       "service": "mdi:shield-airplane" | ||||
|     "alarm_arm_custom_bypass": { | ||||
|       "service": "mdi:security" | ||||
|     }, | ||||
|     "alarm_disarm": { | ||||
|       "service": "mdi:shield-off" | ||||
|     }, | ||||
|     "alarm_trigger": { | ||||
|       "service": "mdi:bell-ring" | ||||
|     }, | ||||
|     "alarm_arm_vacation": { | ||||
|       "service": "mdi:shield-airplane" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| { | ||||
|   "title": "Alarm control panel", | ||||
|   "device_automation": { | ||||
|     "action_type": { | ||||
|       "arm_away": "Arm {entity_name} away", | ||||
| @@ -9,24 +10,24 @@ | ||||
|       "trigger": "Trigger {entity_name}" | ||||
|     }, | ||||
|     "condition_type": { | ||||
|       "is_armed_away": "{entity_name} is armed away", | ||||
|       "is_armed_home": "{entity_name} is armed home", | ||||
|       "is_armed_night": "{entity_name} is armed night", | ||||
|       "is_armed_vacation": "{entity_name} is armed vacation", | ||||
|       "is_triggered": "{entity_name} is triggered", | ||||
|       "is_disarmed": "{entity_name} is disarmed", | ||||
|       "is_triggered": "{entity_name} is triggered" | ||||
|       "is_armed_home": "{entity_name} is armed home", | ||||
|       "is_armed_away": "{entity_name} is armed away", | ||||
|       "is_armed_night": "{entity_name} is armed night", | ||||
|       "is_armed_vacation": "{entity_name} is armed vacation" | ||||
|     }, | ||||
|     "extra_fields": { | ||||
|       "code": "Code", | ||||
|       "for": "[%key:common::device_automation::extra_fields::for%]" | ||||
|     }, | ||||
|     "trigger_type": { | ||||
|       "armed_away": "{entity_name} armed away", | ||||
|       "armed_home": "{entity_name} armed home", | ||||
|       "armed_night": "{entity_name} armed night", | ||||
|       "armed_vacation": "{entity_name} armed vacation", | ||||
|       "triggered": "{entity_name} triggered", | ||||
|       "disarmed": "{entity_name} disarmed", | ||||
|       "triggered": "{entity_name} triggered" | ||||
|       "armed_home": "{entity_name} armed home", | ||||
|       "armed_away": "{entity_name} armed away", | ||||
|       "armed_night": "{entity_name} armed night", | ||||
|       "armed_vacation": "{entity_name} armed vacation" | ||||
|     } | ||||
|   }, | ||||
|   "entity_component": { | ||||
| @@ -34,35 +35,107 @@ | ||||
|       "name": "[%key:component::alarm_control_panel::title%]", | ||||
|       "state": { | ||||
|         "armed": "Armed", | ||||
|         "armed_away": "Armed away", | ||||
|         "armed_custom_bypass": "Armed custom bypass", | ||||
|         "disarmed": "Disarmed", | ||||
|         "armed_home": "Armed home", | ||||
|         "armed_away": "Armed away", | ||||
|         "armed_night": "Armed night", | ||||
|         "armed_vacation": "Armed vacation", | ||||
|         "arming": "Arming", | ||||
|         "disarmed": "Disarmed", | ||||
|         "disarming": "Disarming", | ||||
|         "armed_custom_bypass": "Armed custom bypass", | ||||
|         "pending": "Pending", | ||||
|         "arming": "Arming", | ||||
|         "disarming": "Disarming", | ||||
|         "triggered": "Triggered" | ||||
|       }, | ||||
|       "state_attributes": { | ||||
|         "code_format": { | ||||
|           "name": "Code format", | ||||
|           "state": { | ||||
|             "text": "Text", | ||||
|             "number": "Number" | ||||
|           } | ||||
|         }, | ||||
|         "changed_by": { | ||||
|           "name": "Changed by" | ||||
|         }, | ||||
|         "code_arm_required": { | ||||
|           "name": "Code for arming", | ||||
|           "state": { | ||||
|             "false": "Not required", | ||||
|             "true": "Required" | ||||
|             "true": "Required", | ||||
|             "false": "Not required" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|         "code_format": { | ||||
|           "name": "Code format", | ||||
|           "state": { | ||||
|             "number": "Number", | ||||
|             "text": "Text" | ||||
|   "services": { | ||||
|     "alarm_disarm": { | ||||
|       "name": "Disarm", | ||||
|       "description": "Disarms the alarm.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "name": "Code", | ||||
|           "description": "Code to disarm the alarm." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "alarm_arm_custom_bypass": { | ||||
|       "name": "Arm with custom bypass", | ||||
|       "description": "Arms the alarm while allowing to bypass a custom area.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", | ||||
|           "description": "Code to arm the alarm." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "alarm_arm_home": { | ||||
|       "name": "Arm home", | ||||
|       "description": "Arms the alarm in the home mode.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "alarm_arm_away": { | ||||
|       "name": "Arm away", | ||||
|       "description": "Arms the alarm in the away mode.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "alarm_arm_night": { | ||||
|       "name": "Arm night", | ||||
|       "description": "Arms the alarm in the night mode.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "alarm_arm_vacation": { | ||||
|       "name": "Arm vacation", | ||||
|       "description": "Arms the alarm in the vacation mode.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "alarm_trigger": { | ||||
|       "name": "Trigger", | ||||
|       "description": "Triggers the alarm manually.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @@ -70,78 +143,5 @@ | ||||
|     "code_arm_required": { | ||||
|       "message": "Arming requires a code but none was given for {entity_id}." | ||||
|     } | ||||
|   }, | ||||
|   "services": { | ||||
|     "alarm_arm_away": { | ||||
|       "description": "Arms the alarm in the away mode.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]", | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]" | ||||
|   } | ||||
|       }, | ||||
|       "name": "Arm away" | ||||
|     }, | ||||
|     "alarm_arm_custom_bypass": { | ||||
|       "description": "Arms the alarm while allowing to bypass a custom area.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "description": "Code to arm the alarm.", | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]" | ||||
|         } | ||||
|       }, | ||||
|       "name": "Arm with custom bypass" | ||||
|     }, | ||||
|     "alarm_arm_home": { | ||||
|       "description": "Arms the alarm in the home mode.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]", | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]" | ||||
|         } | ||||
|       }, | ||||
|       "name": "Arm home" | ||||
|     }, | ||||
|     "alarm_arm_night": { | ||||
|       "description": "Arms the alarm in the night mode.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]", | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]" | ||||
|         } | ||||
|       }, | ||||
|       "name": "Arm night" | ||||
|     }, | ||||
|     "alarm_arm_vacation": { | ||||
|       "description": "Arms the alarm in the vacation mode.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]", | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]" | ||||
|         } | ||||
|       }, | ||||
|       "name": "Arm vacation" | ||||
|     }, | ||||
|     "alarm_disarm": { | ||||
|       "description": "Disarms the alarm.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "description": "Code to disarm the alarm.", | ||||
|           "name": "Code" | ||||
|         } | ||||
|       }, | ||||
|       "name": "Disarm" | ||||
|     }, | ||||
|     "alarm_trigger": { | ||||
|       "description": "Triggers the alarm manually.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]", | ||||
|           "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]" | ||||
|         } | ||||
|       }, | ||||
|       "name": "Trigger" | ||||
|     } | ||||
|   }, | ||||
|   "title": "Alarm control panel" | ||||
| } | ||||
|   | ||||
| @@ -1,101 +1,101 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "title": "Choose AlarmDecoder protocol", | ||||
|         "data": { | ||||
|           "protocol": "Protocol" | ||||
|         } | ||||
|       }, | ||||
|     "create_entry": { | ||||
|       "default": "Successfully connected to AlarmDecoder." | ||||
|       "protocol": { | ||||
|         "title": "Configure connection settings", | ||||
|         "data": { | ||||
|           "host": "[%key:common::config_flow::data::host%]", | ||||
|           "port": "[%key:common::config_flow::data::port%]", | ||||
|           "device_baudrate": "Device baud rate", | ||||
|           "device_path": "Device path" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.", | ||||
|           "port": "The port on which AlarmDecoder is accessible (for example, 10000)" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "step": { | ||||
|       "protocol": { | ||||
|         "data": { | ||||
|           "device_baudrate": "Device baud rate", | ||||
|           "device_path": "Device path", | ||||
|           "host": "[%key:common::config_flow::data::host%]", | ||||
|           "port": "[%key:common::config_flow::data::port%]" | ||||
|     "create_entry": { | ||||
|       "default": "Successfully connected to AlarmDecoder." | ||||
|     }, | ||||
|         "data_description": { | ||||
|           "host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.", | ||||
|           "port": "The port on which AlarmDecoder is accessible (for example, 10000)" | ||||
|         }, | ||||
|         "title": "Configure connection settings" | ||||
|       }, | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "protocol": "Protocol" | ||||
|         }, | ||||
|         "title": "Choose AlarmDecoder protocol" | ||||
|       } | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     } | ||||
|   }, | ||||
|   "options": { | ||||
|     "error": { | ||||
|       "int": "The field below must be an integer.", | ||||
|       "loop_range": "'RF loop' must be an integer between 1 and 4.", | ||||
|       "loop_rfid": "'RF loop' cannot be used without 'RF serial'.", | ||||
|       "relay_inclusive": "'Relay address' and 'Relay channel' are codependent and must be included together." | ||||
|     }, | ||||
|     "step": { | ||||
|       "arm_settings": { | ||||
|         "data": { | ||||
|           "alt_night_mode": "Alternative night mode", | ||||
|           "auto_bypass": "Auto-bypass on arm", | ||||
|           "code_arm_required": "Code required for arming" | ||||
|         }, | ||||
|         "title": "[%key:component::alarmdecoder::options::step::init::title%]" | ||||
|       }, | ||||
|       "init": { | ||||
|         "title": "Configure AlarmDecoder", | ||||
|         "description": "What would you like to edit?", | ||||
|         "data": { | ||||
|           "edit_selection": "Edit" | ||||
|         } | ||||
|       }, | ||||
|         "description": "What would you like to edit?", | ||||
|         "title": "Configure AlarmDecoder" | ||||
|       }, | ||||
|       "zone_details": { | ||||
|       "arm_settings": { | ||||
|         "title": "[%key:component::alarmdecoder::options::step::init::title%]", | ||||
|         "data": { | ||||
|           "zone_loop": "RF loop", | ||||
|           "zone_name": "Zone name", | ||||
|           "zone_relayaddr": "Relay address", | ||||
|           "zone_relaychan": "Relay channel", | ||||
|           "zone_rfid": "RF serial", | ||||
|           "zone_type": "Zone type" | ||||
|         }, | ||||
|         "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave 'Zone name' blank.", | ||||
|         "title": "[%key:component::alarmdecoder::options::step::init::title%]" | ||||
|           "auto_bypass": "Auto-bypass on arm", | ||||
|           "code_arm_required": "Code required for arming", | ||||
|           "alt_night_mode": "Alternative night mode" | ||||
|         } | ||||
|       }, | ||||
|       "zone_select": { | ||||
|         "title": "[%key:component::alarmdecoder::options::step::init::title%]", | ||||
|         "description": "Enter the zone number you'd like to to add, edit, or remove.", | ||||
|         "data": { | ||||
|           "zone_number": "Zone number" | ||||
|         }, | ||||
|         "description": "Enter the zone number you'd like to to add, edit, or remove.", | ||||
|         "title": "[%key:component::alarmdecoder::options::step::init::title%]" | ||||
|         } | ||||
|       }, | ||||
|       "zone_details": { | ||||
|         "title": "[%key:component::alarmdecoder::options::step::init::title%]", | ||||
|         "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave 'Zone name' blank.", | ||||
|         "data": { | ||||
|           "zone_name": "Zone name", | ||||
|           "zone_type": "Zone type", | ||||
|           "zone_rfid": "RF serial", | ||||
|           "zone_loop": "RF loop", | ||||
|           "zone_relayaddr": "Relay address", | ||||
|           "zone_relaychan": "Relay channel" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "error": { | ||||
|       "relay_inclusive": "'Relay address' and 'Relay channel' are codependent and must be included together.", | ||||
|       "int": "The field below must be an integer.", | ||||
|       "loop_rfid": "'RF loop' cannot be used without 'RF serial'.", | ||||
|       "loop_range": "'RF loop' must be an integer between 1 and 4." | ||||
|     } | ||||
|   }, | ||||
|   "services": { | ||||
|     "alarm_keypress": { | ||||
|       "name": "Key press", | ||||
|       "description": "Sends custom keypresses to the alarm.", | ||||
|       "fields": { | ||||
|         "keypress": { | ||||
|           "description": "String to send to the alarm panel.", | ||||
|           "name": "[%key:component::alarmdecoder::services::alarm_keypress::name%]" | ||||
|           "name": "[%key:component::alarmdecoder::services::alarm_keypress::name%]", | ||||
|           "description": "String to send to the alarm panel." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|       "name": "Key press" | ||||
|     }, | ||||
|     "alarm_toggle_chime": { | ||||
|       "name": "Toggle chime", | ||||
|       "description": "Sends the alarm the toggle chime command.", | ||||
|       "fields": { | ||||
|         "code": { | ||||
|           "description": "Code to toggle the alarm control panel chime with.", | ||||
|           "name": "Code" | ||||
|           "name": "Code", | ||||
|           "description": "Code to toggle the alarm control panel chime with." | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|       "name": "Toggle chime" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| { | ||||
|   "title": "Alert", | ||||
|   "entity_component": { | ||||
|     "_": { | ||||
|       "name": "[%key:component::alert::title%]", | ||||
| @@ -11,17 +12,16 @@ | ||||
|   }, | ||||
|   "services": { | ||||
|     "toggle": { | ||||
|       "description": "Toggles alert's notifications.", | ||||
|       "name": "[%key:common::action::toggle%]" | ||||
|       "name": "[%key:common::action::toggle%]", | ||||
|       "description": "Toggles alert's notifications." | ||||
|     }, | ||||
|     "turn_off": { | ||||
|       "description": "Silences alert's notifications.", | ||||
|       "name": "[%key:common::action::turn_off%]" | ||||
|       "name": "[%key:common::action::turn_off%]", | ||||
|       "description": "Silences alert's notifications." | ||||
|     }, | ||||
|     "turn_on": { | ||||
|       "description": "Resets alert's notifications.", | ||||
|       "name": "[%key:common::action::turn_on%]" | ||||
|       "name": "[%key:common::action::turn_on%]", | ||||
|       "description": "Resets alert's notifications." | ||||
|     } | ||||
|   } | ||||
|   }, | ||||
|   "title": "Alert" | ||||
| } | ||||
|   | ||||
| @@ -1472,10 +1472,10 @@ class AlexaModeController(AlexaCapability): | ||||
|             # Return state instead of position when using ModeController. | ||||
|             mode = self.entity.state | ||||
|             if mode in ( | ||||
|                 cover.CoverState.OPEN, | ||||
|                 cover.CoverState.OPENING, | ||||
|                 cover.CoverState.CLOSED, | ||||
|                 cover.CoverState.CLOSING, | ||||
|                 cover.STATE_OPEN, | ||||
|                 cover.STATE_OPENING, | ||||
|                 cover.STATE_CLOSED, | ||||
|                 cover.STATE_CLOSING, | ||||
|                 STATE_UNKNOWN, | ||||
|             ): | ||||
|                 return f"{cover.ATTR_POSITION}.{mode}" | ||||
| @@ -1594,11 +1594,11 @@ class AlexaModeController(AlexaCapability): | ||||
|                 ["Position", AlexaGlobalCatalog.SETTING_OPENING], False | ||||
|             ) | ||||
|             self._resource.add_mode( | ||||
|                 f"{cover.ATTR_POSITION}.{cover.CoverState.OPEN}", | ||||
|                 f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", | ||||
|                 [AlexaGlobalCatalog.VALUE_OPEN], | ||||
|             ) | ||||
|             self._resource.add_mode( | ||||
|                 f"{cover.ATTR_POSITION}.{cover.CoverState.CLOSED}", | ||||
|                 f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", | ||||
|                 [AlexaGlobalCatalog.VALUE_CLOSE], | ||||
|             ) | ||||
|             self._resource.add_mode( | ||||
| @@ -1651,22 +1651,22 @@ class AlexaModeController(AlexaCapability): | ||||
|                 raise_labels.append(AlexaSemantics.ACTION_OPEN) | ||||
|                 self._semantics.add_states_to_value( | ||||
|                     [AlexaSemantics.STATES_CLOSED], | ||||
|                     f"{cover.ATTR_POSITION}.{cover.CoverState.CLOSED}", | ||||
|                     f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", | ||||
|                 ) | ||||
|                 self._semantics.add_states_to_value( | ||||
|                     [AlexaSemantics.STATES_OPEN], | ||||
|                     f"{cover.ATTR_POSITION}.{cover.CoverState.OPEN}", | ||||
|                     f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", | ||||
|                 ) | ||||
|  | ||||
|             self._semantics.add_action_to_directive( | ||||
|                 lower_labels, | ||||
|                 "SetMode", | ||||
|                 {"mode": f"{cover.ATTR_POSITION}.{cover.CoverState.CLOSED}"}, | ||||
|                 {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"}, | ||||
|             ) | ||||
|             self._semantics.add_action_to_directive( | ||||
|                 raise_labels, | ||||
|                 "SetMode", | ||||
|                 {"mode": f"{cover.ATTR_POSITION}.{cover.CoverState.OPEN}"}, | ||||
|                 {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"}, | ||||
|             ) | ||||
|  | ||||
|             return self._semantics.serialize_semantics() | ||||
|   | ||||
| @@ -1261,9 +1261,9 @@ async def async_api_set_mode( | ||||
|     elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": | ||||
|         position = mode.split(".")[1] | ||||
|  | ||||
|         if position == cover.CoverState.CLOSED: | ||||
|         if position == cover.STATE_CLOSED: | ||||
|             service = cover.SERVICE_CLOSE_COVER | ||||
|         elif position == cover.CoverState.OPEN: | ||||
|         elif position == cover.STATE_OPEN: | ||||
|             service = cover.SERVICE_OPEN_COVER | ||||
|         elif position == "custom": | ||||
|             service = cover.SERVICE_STOP_COVER | ||||
|   | ||||
| @@ -18,9 +18,7 @@ from homeassistant.components.binary_sensor import ( | ||||
| from homeassistant.const import EntityCategory | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||
| import homeassistant.helpers.entity_registry as er | ||||
|  | ||||
| from .const import _LOGGER, DOMAIN | ||||
| from .coordinator import AmazonConfigEntry | ||||
| from .entity import AmazonEntity | ||||
| from .utils import async_update_unique_id | ||||
| @@ -53,47 +51,11 @@ BINARY_SENSORS: Final = ( | ||||
|         ), | ||||
|         is_supported=lambda device, key: device.sensors.get(key) is not None, | ||||
|         is_available_fn=lambda device, key: ( | ||||
|             device.online | ||||
|             and (sensor := device.sensors.get(key)) is not None | ||||
|             and sensor.error is False | ||||
|             device.online and device.sensors[key].error is False | ||||
|         ), | ||||
|     ), | ||||
| ) | ||||
|  | ||||
| DEPRECATED_BINARY_SENSORS: Final = ( | ||||
|     AmazonBinarySensorEntityDescription( | ||||
|         key="bluetooth", | ||||
|         entity_category=EntityCategory.DIAGNOSTIC, | ||||
|         translation_key="bluetooth", | ||||
|         is_on_fn=lambda device, key: False, | ||||
|     ), | ||||
|     AmazonBinarySensorEntityDescription( | ||||
|         key="babyCryDetectionState", | ||||
|         translation_key="baby_cry_detection", | ||||
|         is_on_fn=lambda device, key: False, | ||||
|     ), | ||||
|     AmazonBinarySensorEntityDescription( | ||||
|         key="beepingApplianceDetectionState", | ||||
|         translation_key="beeping_appliance_detection", | ||||
|         is_on_fn=lambda device, key: False, | ||||
|     ), | ||||
|     AmazonBinarySensorEntityDescription( | ||||
|         key="coughDetectionState", | ||||
|         translation_key="cough_detection", | ||||
|         is_on_fn=lambda device, key: False, | ||||
|     ), | ||||
|     AmazonBinarySensorEntityDescription( | ||||
|         key="dogBarkDetectionState", | ||||
|         translation_key="dog_bark_detection", | ||||
|         is_on_fn=lambda device, key: False, | ||||
|     ), | ||||
|     AmazonBinarySensorEntityDescription( | ||||
|         key="waterSoundsDetectionState", | ||||
|         translation_key="water_sounds_detection", | ||||
|         is_on_fn=lambda device, key: False, | ||||
|     ), | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def async_setup_entry( | ||||
|     hass: HomeAssistant, | ||||
| @@ -104,8 +66,6 @@ async def async_setup_entry( | ||||
|  | ||||
|     coordinator = entry.runtime_data | ||||
|  | ||||
|     entity_registry = er.async_get(hass) | ||||
|  | ||||
|     # Replace unique id for "detectionState" binary sensor | ||||
|     await async_update_unique_id( | ||||
|         hass, | ||||
| @@ -115,16 +75,6 @@ async def async_setup_entry( | ||||
|         "detectionState", | ||||
|     ) | ||||
|  | ||||
|     # Clean up deprecated sensors | ||||
|     for sensor_desc in DEPRECATED_BINARY_SENSORS: | ||||
|         for serial_num in coordinator.data: | ||||
|             unique_id = f"{serial_num}-{sensor_desc.key}" | ||||
|             if entity_id := entity_registry.async_get_entity_id( | ||||
|                 BINARY_SENSOR_DOMAIN, DOMAIN, unique_id | ||||
|             ): | ||||
|                 _LOGGER.debug("Removing deprecated entity %s", entity_id) | ||||
|                 entity_registry.async_remove(entity_id) | ||||
|  | ||||
|     known_devices: set[str] = set() | ||||
|  | ||||
|     def _check_device() -> None: | ||||
|   | ||||
| @@ -26,6 +26,3 @@ COUNTRY_DOMAINS = { | ||||
|     "us": DEFAULT_DOMAIN, | ||||
|     "za": "co.za", | ||||
| } | ||||
|  | ||||
| CATEGORY_SENSORS = "sensors" | ||||
| CATEGORY_NOTIFICATIONS = "notifications" | ||||
|   | ||||
| @@ -8,5 +8,5 @@ | ||||
|   "iot_class": "cloud_polling", | ||||
|   "loggers": ["aioamazondevices"], | ||||
|   "quality_scale": "platinum", | ||||
|   "requirements": ["aioamazondevices==6.5.5"] | ||||
|   "requirements": ["aioamazondevices==6.2.7"] | ||||
| } | ||||
|   | ||||
| @@ -4,15 +4,9 @@ from __future__ import annotations | ||||
|  | ||||
| from collections.abc import Callable | ||||
| from dataclasses import dataclass | ||||
| from datetime import datetime | ||||
| from typing import Final | ||||
|  | ||||
| from aioamazondevices.api import AmazonDevice | ||||
| from aioamazondevices.const import ( | ||||
|     NOTIFICATION_ALARM, | ||||
|     NOTIFICATION_REMINDER, | ||||
|     NOTIFICATION_TIMER, | ||||
| ) | ||||
|  | ||||
| from homeassistant.components.sensor import ( | ||||
|     SensorDeviceClass, | ||||
| @@ -25,7 +19,6 @@ from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||
| from homeassistant.helpers.typing import StateType | ||||
|  | ||||
| from .const import CATEGORY_NOTIFICATIONS, CATEGORY_SENSORS | ||||
| from .coordinator import AmazonConfigEntry | ||||
| from .entity import AmazonEntity | ||||
|  | ||||
| @@ -39,33 +32,17 @@ class AmazonSensorEntityDescription(SensorEntityDescription): | ||||
|  | ||||
|     native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None | ||||
|     is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: ( | ||||
|         device.online | ||||
|         and (sensor := device.sensors.get(key)) is not None | ||||
|         and sensor.error is False | ||||
|         device.online and device.sensors[key].error is False | ||||
|     ) | ||||
|     category: str = CATEGORY_SENSORS | ||||
|  | ||||
|  | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class AmazonNotificationEntityDescription(SensorEntityDescription): | ||||
|     """Amazon Devices notification entity description.""" | ||||
|  | ||||
|     native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None | ||||
|     is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: ( | ||||
|         device.online | ||||
|         and (notification := device.notifications.get(key)) is not None | ||||
|         and notification.next_occurrence is not None | ||||
|     ) | ||||
|     category: str = CATEGORY_NOTIFICATIONS | ||||
|  | ||||
|  | ||||
| SENSORS: Final = ( | ||||
|     AmazonSensorEntityDescription( | ||||
|         key="temperature", | ||||
|         device_class=SensorDeviceClass.TEMPERATURE, | ||||
|         native_unit_of_measurement_fn=lambda device, key: ( | ||||
|         native_unit_of_measurement_fn=lambda device, _key: ( | ||||
|             UnitOfTemperature.CELSIUS | ||||
|             if key in device.sensors and device.sensors[key].scale == "CELSIUS" | ||||
|             if device.sensors[_key].scale == "CELSIUS" | ||||
|             else UnitOfTemperature.FAHRENHEIT | ||||
|         ), | ||||
|         state_class=SensorStateClass.MEASUREMENT, | ||||
| @@ -77,23 +54,6 @@ SENSORS: Final = ( | ||||
|         state_class=SensorStateClass.MEASUREMENT, | ||||
|     ), | ||||
| ) | ||||
| NOTIFICATIONS: Final = ( | ||||
|     AmazonNotificationEntityDescription( | ||||
|         key=NOTIFICATION_ALARM, | ||||
|         translation_key="alarm", | ||||
|         device_class=SensorDeviceClass.TIMESTAMP, | ||||
|     ), | ||||
|     AmazonNotificationEntityDescription( | ||||
|         key=NOTIFICATION_REMINDER, | ||||
|         translation_key="reminder", | ||||
|         device_class=SensorDeviceClass.TIMESTAMP, | ||||
|     ), | ||||
|     AmazonNotificationEntityDescription( | ||||
|         key=NOTIFICATION_TIMER, | ||||
|         translation_key="timer", | ||||
|         device_class=SensorDeviceClass.TIMESTAMP, | ||||
|     ), | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def async_setup_entry( | ||||
| @@ -112,18 +72,12 @@ async def async_setup_entry( | ||||
|         new_devices = current_devices - known_devices | ||||
|         if new_devices: | ||||
|             known_devices.update(new_devices) | ||||
|             sensors_list = [ | ||||
|             async_add_entities( | ||||
|                 AmazonSensorEntity(coordinator, serial_num, sensor_desc) | ||||
|                 for sensor_desc in SENSORS | ||||
|                 for serial_num in new_devices | ||||
|                 if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None | ||||
|             ] | ||||
|             notifications_list = [ | ||||
|                 AmazonSensorEntity(coordinator, serial_num, notification_desc) | ||||
|                 for notification_desc in NOTIFICATIONS | ||||
|                 for serial_num in new_devices | ||||
|             ] | ||||
|             async_add_entities(sensors_list + notifications_list) | ||||
|             ) | ||||
|  | ||||
|     _check_device() | ||||
|     entry.async_on_unload(coordinator.async_add_listener(_check_device)) | ||||
| @@ -132,9 +86,7 @@ async def async_setup_entry( | ||||
| class AmazonSensorEntity(AmazonEntity, SensorEntity): | ||||
|     """Sensor device.""" | ||||
|  | ||||
|     entity_description: ( | ||||
|         AmazonSensorEntityDescription | AmazonNotificationEntityDescription | ||||
|     ) | ||||
|     entity_description: AmazonSensorEntityDescription | ||||
|  | ||||
|     @property | ||||
|     def native_unit_of_measurement(self) -> str | None: | ||||
| @@ -147,13 +99,9 @@ class AmazonSensorEntity(AmazonEntity, SensorEntity): | ||||
|         return super().native_unit_of_measurement | ||||
|  | ||||
|     @property | ||||
|     def native_value(self) -> StateType | datetime: | ||||
|     def native_value(self) -> StateType: | ||||
|         """Return the state of the sensor.""" | ||||
|         # Sensors | ||||
|         if self.entity_description.category == CATEGORY_SENSORS: | ||||
|         return self.device.sensors[self.entity_description.key].value | ||||
|         # Notifications | ||||
|         return self.device.notifications[self.entity_description.key].next_occurrence | ||||
|  | ||||
|     @property | ||||
|     def available(self) -> bool: | ||||
|   | ||||
| @@ -1,12 +1,47 @@ | ||||
| { | ||||
|   "common": { | ||||
|     "data_code": "One-time password (OTP code)", | ||||
|     "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.", | ||||
|     "data_description_password": "The password of your Amazon account.", | ||||
|     "data_description_username": "The email address of your Amazon account.", | ||||
|     "data_description_password": "The password of your Amazon account.", | ||||
|     "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.", | ||||
|     "device_id_description": "The ID of the device to send the command to." | ||||
|   }, | ||||
|   "config": { | ||||
|     "flow_title": "{username}", | ||||
|     "step": { | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "username": "[%key:common::config_flow::data::username%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]", | ||||
|           "code": "[%key:component::alexa_devices::common::data_code%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "username": "[%key:component::alexa_devices::common::data_description_username%]", | ||||
|           "password": "[%key:component::alexa_devices::common::data_description_password%]", | ||||
|           "code": "[%key:component::alexa_devices::common::data_description_code%]" | ||||
|         } | ||||
|       }, | ||||
|       "reauth_confirm": { | ||||
|         "data": { | ||||
|           "password": "[%key:common::config_flow::data::password%]", | ||||
|           "code": "[%key:component::alexa_devices::common::data_code%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "password": "[%key:component::alexa_devices::common::data_description_password%]", | ||||
|           "code": "[%key:component::alexa_devices::common::data_description_code%]" | ||||
|         } | ||||
|       }, | ||||
|       "reconfigure": { | ||||
|         "data": { | ||||
|           "password": "[%key:common::config_flow::data::password%]", | ||||
|           "code": "[%key:component::alexa_devices::common::data_code%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "password": "[%key:component::alexa_devices::common::data_description_password%]", | ||||
|           "code": "[%key:component::alexa_devices::common::data_description_code%]" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", | ||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||
| @@ -20,61 +55,15 @@ | ||||
|       "cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.", | ||||
|       "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||
|     }, | ||||
|     "flow_title": "{username}", | ||||
|     "step": { | ||||
|       "reauth_confirm": { | ||||
|         "data": { | ||||
|           "code": "[%key:component::alexa_devices::common::data_code%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "code": "[%key:component::alexa_devices::common::data_description_code%]", | ||||
|           "password": "[%key:component::alexa_devices::common::data_description_password%]" | ||||
|         } | ||||
|       }, | ||||
|       "reconfigure": { | ||||
|         "data": { | ||||
|           "code": "[%key:component::alexa_devices::common::data_code%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "code": "[%key:component::alexa_devices::common::data_description_code%]", | ||||
|           "password": "[%key:component::alexa_devices::common::data_description_password%]" | ||||
|         } | ||||
|       }, | ||||
|       "user": { | ||||
|         "data": { | ||||
|           "code": "[%key:component::alexa_devices::common::data_code%]", | ||||
|           "password": "[%key:common::config_flow::data::password%]", | ||||
|           "username": "[%key:common::config_flow::data::username%]" | ||||
|         }, | ||||
|         "data_description": { | ||||
|           "code": "[%key:component::alexa_devices::common::data_description_code%]", | ||||
|           "password": "[%key:component::alexa_devices::common::data_description_password%]", | ||||
|           "username": "[%key:component::alexa_devices::common::data_description_username%]" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
|     "notify": { | ||||
|       "announce": { | ||||
|         "name": "Announce" | ||||
|       }, | ||||
|       "speak": { | ||||
|         "name": "Speak" | ||||
|       } | ||||
|       }, | ||||
|     "sensor": { | ||||
|       "alarm": { | ||||
|         "name": "Next alarm" | ||||
|       }, | ||||
|       "reminder": { | ||||
|         "name": "Next reminder" | ||||
|       }, | ||||
|       "timer": { | ||||
|         "name": "Next timer" | ||||
|       "announce": { | ||||
|         "name": "Announce" | ||||
|       } | ||||
|     }, | ||||
|     "switch": { | ||||
| @@ -83,24 +72,34 @@ | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "exceptions": { | ||||
|     "cannot_connect_with_error": { | ||||
|       "message": "Error connecting: {error}" | ||||
|   "services": { | ||||
|     "send_sound": { | ||||
|       "name": "Send sound", | ||||
|       "description": "Sends a sound to a device", | ||||
|       "fields": { | ||||
|         "device_id": { | ||||
|           "name": "Device", | ||||
|           "description": "[%key:component::alexa_devices::common::device_id_description%]" | ||||
|         }, | ||||
|     "cannot_retrieve_data_with_error": { | ||||
|       "message": "Error retrieving data: {error}" | ||||
|         "sound": { | ||||
|           "name": "Alexa Skill sound file", | ||||
|           "description": "The sound file to play." | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "device_serial_number_missing": { | ||||
|       "message": "Device serial number missing: {device_id}" | ||||
|     "send_text_command": { | ||||
|       "name": "Send text command", | ||||
|       "description": "Sends a text command to a device", | ||||
|       "fields": { | ||||
|         "text_command": { | ||||
|           "name": "Alexa text command", | ||||
|           "description": "The text command to send." | ||||
|         }, | ||||
|     "entry_not_loaded": { | ||||
|       "message": "Entry not loaded: {entry}" | ||||
|     }, | ||||
|     "invalid_device_id": { | ||||
|       "message": "Invalid device ID specified: {device_id}" | ||||
|     }, | ||||
|     "invalid_sound_value": { | ||||
|       "message": "Invalid sound {sound} specified" | ||||
|         "device_id": { | ||||
|           "name": "Device", | ||||
|           "description": "[%key:component::alexa_devices::common::device_id_description%]" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "selector": { | ||||
| @@ -150,34 +149,24 @@ | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "services": { | ||||
|     "send_sound": { | ||||
|       "description": "Sends a sound to a device", | ||||
|       "fields": { | ||||
|         "device_id": { | ||||
|           "description": "[%key:component::alexa_devices::common::device_id_description%]", | ||||
|           "name": "Device" | ||||
|   "exceptions": { | ||||
|     "cannot_connect_with_error": { | ||||
|       "message": "Error connecting: {error}" | ||||
|     }, | ||||
|         "sound": { | ||||
|           "description": "The sound file to play.", | ||||
|           "name": "Alexa Skill sound file" | ||||
|         } | ||||
|     "cannot_retrieve_data_with_error": { | ||||
|       "message": "Error retrieving data: {error}" | ||||
|     }, | ||||
|       "name": "Send sound" | ||||
|     "device_serial_number_missing": { | ||||
|       "message": "Device serial number missing: {device_id}" | ||||
|     }, | ||||
|     "send_text_command": { | ||||
|       "description": "Sends a text command to a device", | ||||
|       "fields": { | ||||
|         "device_id": { | ||||
|           "description": "[%key:component::alexa_devices::common::device_id_description%]", | ||||
|           "name": "Device" | ||||
|     "invalid_device_id": { | ||||
|       "message": "Invalid device ID specified: {device_id}" | ||||
|     }, | ||||
|         "text_command": { | ||||
|           "description": "The text command to send.", | ||||
|           "name": "Alexa text command" | ||||
|         } | ||||
|     "invalid_sound_value": { | ||||
|       "message": "Invalid sound {sound} specified" | ||||
|     }, | ||||
|       "name": "Send text command" | ||||
|     "entry_not_loaded": { | ||||
|       "message": "Entry not loaded: {entry}" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -18,11 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||
|  | ||||
| from .coordinator import AmazonConfigEntry | ||||
| from .entity import AmazonEntity | ||||
| from .utils import ( | ||||
|     alexa_api_call, | ||||
|     async_remove_dnd_from_virtual_group, | ||||
|     async_update_unique_id, | ||||
| ) | ||||
| from .utils import alexa_api_call, async_update_unique_id | ||||
|  | ||||
| PARALLEL_UPDATES = 1 | ||||
|  | ||||
| @@ -33,9 +29,7 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription): | ||||
|  | ||||
|     is_on_fn: Callable[[AmazonDevice], bool] | ||||
|     is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: ( | ||||
|         device.online | ||||
|         and (sensor := device.sensors.get(key)) is not None | ||||
|         and sensor.error is False | ||||
|         device.online and device.sensors[key].error is False | ||||
|     ) | ||||
|     method: str | ||||
|  | ||||
| @@ -64,9 +58,6 @@ async def async_setup_entry( | ||||
|         hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd" | ||||
|     ) | ||||
|  | ||||
|     # Remove DND switch from virtual groups | ||||
|     await async_remove_dnd_from_virtual_group(hass, coordinator) | ||||
|  | ||||
|     known_devices: set[str] = set() | ||||
|  | ||||
|     def _check_device() -> None: | ||||
|   | ||||
| @@ -4,10 +4,8 @@ from collections.abc import Awaitable, Callable, Coroutine | ||||
| from functools import wraps | ||||
| from typing import Any, Concatenate | ||||
|  | ||||
| from aioamazondevices.const import SPEAKER_GROUP_FAMILY | ||||
| from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData | ||||
|  | ||||
| from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.exceptions import HomeAssistantError | ||||
| import homeassistant.helpers.entity_registry as er | ||||
| @@ -63,21 +61,3 @@ async def async_update_unique_id( | ||||
|  | ||||
|             # Update the registry with the new unique_id | ||||
|             entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) | ||||
|  | ||||
|  | ||||
| async def async_remove_dnd_from_virtual_group( | ||||
|     hass: HomeAssistant, | ||||
|     coordinator: AmazonDevicesCoordinator, | ||||
| ) -> None: | ||||
|     """Remove entity DND from virtual group.""" | ||||
|     entity_registry = er.async_get(hass) | ||||
|  | ||||
|     for serial_num in coordinator.data: | ||||
|         unique_id = f"{serial_num}-do_not_disturb" | ||||
|         entity_id = entity_registry.async_get_entity_id( | ||||
|             DOMAIN, SWITCH_DOMAIN, unique_id | ||||
|         ) | ||||
|         is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY | ||||
|         if entity_id and is_group: | ||||
|             entity_registry.async_remove(entity_id) | ||||
|             _LOGGER.debug("Removed DND switch from virtual group %s", entity_id) | ||||
|   | ||||
| @@ -65,31 +65,6 @@ SENSOR_DESCRIPTIONS = [ | ||||
|         suggested_display_precision=2, | ||||
|         translation_placeholders={"sensor_name": "BME280"}, | ||||
|     ), | ||||
|     AltruistSensorEntityDescription( | ||||
|         device_class=SensorDeviceClass.HUMIDITY, | ||||
|         key="BME680_humidity", | ||||
|         translation_key="humidity", | ||||
|         native_unit_of_measurement=PERCENTAGE, | ||||
|         suggested_display_precision=2, | ||||
|         translation_placeholders={"sensor_name": "BME680"}, | ||||
|     ), | ||||
|     AltruistSensorEntityDescription( | ||||
|         device_class=SensorDeviceClass.PRESSURE, | ||||
|         key="BME680_pressure", | ||||
|         translation_key="pressure", | ||||
|         native_unit_of_measurement=UnitOfPressure.PA, | ||||
|         suggested_unit_of_measurement=UnitOfPressure.MMHG, | ||||
|         suggested_display_precision=0, | ||||
|         translation_placeholders={"sensor_name": "BME680"}, | ||||
|     ), | ||||
|     AltruistSensorEntityDescription( | ||||
|         device_class=SensorDeviceClass.TEMPERATURE, | ||||
|         key="BME680_temperature", | ||||
|         translation_key="temperature", | ||||
|         native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||||
|         suggested_display_precision=2, | ||||
|         translation_placeholders={"sensor_name": "BME680"}, | ||||
|     ), | ||||
|     AltruistSensorEntityDescription( | ||||
|         device_class=SensorDeviceClass.PRESSURE, | ||||
|         key="BMP_pressure", | ||||
|   | ||||
| @@ -1,12 +1,5 @@ | ||||
| { | ||||
|   "config": { | ||||
|     "abort": { | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", | ||||
|       "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "no_device_found": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     }, | ||||
|     "flow_title": "{name}", | ||||
|     "step": { | ||||
|       "discovery_confirm": { | ||||
| @@ -21,30 +14,37 @@ | ||||
|         }, | ||||
|         "description": "Fill in Altruist IP address or hostname in your local network" | ||||
|       } | ||||
|     }, | ||||
|     "abort": { | ||||
|       "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", | ||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||
|     }, | ||||
|     "error": { | ||||
|       "no_device_found": "[%key:common::config_flow::error::cannot_connect%]" | ||||
|     } | ||||
|   }, | ||||
|   "entity": { | ||||
|     "sensor": { | ||||
|       "co2": { | ||||
|         "name": "{sensor_name} CO2" | ||||
|       }, | ||||
|       "humidity": { | ||||
|         "name": "{sensor_name} humidity" | ||||
|       }, | ||||
|       "noise_avg": { | ||||
|         "name": "Average noise" | ||||
|       }, | ||||
|       "noise_max": { | ||||
|         "name": "Maximum noise" | ||||
|       }, | ||||
|       "pressure": { | ||||
|         "name": "{sensor_name} pressure" | ||||
|       }, | ||||
|       "radiation": { | ||||
|         "name": "Radiation level" | ||||
|       }, | ||||
|       "temperature": { | ||||
|         "name": "{sensor_name} temperature" | ||||
|       }, | ||||
|       "noise_max": { | ||||
|         "name": "Maximum noise" | ||||
|       }, | ||||
|       "noise_avg": { | ||||
|         "name": "Average noise" | ||||
|       }, | ||||
|       "co2": { | ||||
|         "name": "{sensor_name} CO2" | ||||
|       }, | ||||
|       "radiation": { | ||||
|         "name": "Radiation level" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,26 +1,26 @@ | ||||
| { | ||||
|   "entity": { | ||||
|     "binary_sensor": { | ||||
|       "demand_window": { | ||||
|         "default": "mdi:meter-electric", | ||||
|         "state": { | ||||
|           "off": "mdi:meter-electric-outline" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "sensor": { | ||||
|       "general": { | ||||
|         "default": "mdi:transmission-tower" | ||||
|       }, | ||||
|       "controlled_load": { | ||||
|         "default": "mdi:clock-outline" | ||||
|       }, | ||||
|       "feed_in": { | ||||
|         "default": "mdi:solar-power" | ||||
|       }, | ||||
|       "general": { | ||||
|         "default": "mdi:transmission-tower" | ||||
|       }, | ||||
|       "renewables": { | ||||
|         "default": "mdi:solar-power" | ||||
|       } | ||||
|     }, | ||||
|     "binary_sensor": { | ||||
|       "demand_window": { | ||||
|         "default": "mdi:meter-electric", | ||||
|         "state": { | ||||
|           "off": "mdi:meter-electric-outline" | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "services": { | ||||
|   | ||||
| @@ -22,40 +22,40 @@ | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "exceptions": { | ||||
|     "channel_not_found": { | ||||
|       "message": "There is no {channel_type} channel at this site." | ||||
|   "services": { | ||||
|     "get_forecasts": { | ||||
|       "name": "Get price forecasts", | ||||
|       "description": "Retrieves price forecasts from Amber Electric for a site.", | ||||
|       "fields": { | ||||
|         "config_entry_id": { | ||||
|           "description": "The config entry of the site to get forecasts for.", | ||||
|           "name": "Config entry" | ||||
|         }, | ||||
|         "channel_type": { | ||||
|           "name": "Channel type", | ||||
|           "description": "The channel to get forecasts for." | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "exceptions": { | ||||
|     "integration_not_found": { | ||||
|       "message": "Config entry \"{target}\" not found in registry." | ||||
|     }, | ||||
|     "not_loaded": { | ||||
|       "message": "{target} is not loaded." | ||||
|     }, | ||||
|     "channel_not_found": { | ||||
|       "message": "There is no {channel_type} channel at this site." | ||||
|     } | ||||
|   }, | ||||
|   "selector": { | ||||
|     "channel_type": { | ||||
|       "options": { | ||||
|         "general": "General", | ||||
|         "controlled_load": "Controlled load", | ||||
|         "feed_in": "Feed-in", | ||||
|         "general": "General" | ||||
|         "feed_in": "Feed-in" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "services": { | ||||
|     "get_forecasts": { | ||||
|       "description": "Retrieves price forecasts from Amber Electric for a site.", | ||||
|       "fields": { | ||||
|         "channel_type": { | ||||
|           "description": "The channel to get forecasts for.", | ||||
|           "name": "Channel type" | ||||
|         }, | ||||
|         "config_entry_id": { | ||||
|           "description": "The config entry of the site to get forecasts for.", | ||||
|           "name": "Config entry" | ||||
|         } | ||||
|       }, | ||||
|       "name": "Get price forecasts" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user