diff --git a/.core_files.yaml b/.core_files.yaml index 55b543a333e..df69df45cb6 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -52,6 +52,7 @@ components: &components - homeassistant/components/auth/** - homeassistant/components/automation/** - homeassistant/components/backup/** + - homeassistant/components/bluetooth/** - homeassistant/components/cloud/** - homeassistant/components/config/** - homeassistant/components/configurator/** @@ -87,6 +88,7 @@ components: &components - homeassistant/components/persistent_notification/** - homeassistant/components/person/** - homeassistant/components/recorder/** + - homeassistant/components/repairs/** - homeassistant/components/safe_mode/** - homeassistant/components/script/** - homeassistant/components/shopping_list/** diff --git a/.coveragerc b/.coveragerc index f52631a57bd..d529cdbd9ca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -23,6 +23,7 @@ omit = homeassistant/components/adax/climate.py homeassistant/components/adguard/__init__.py homeassistant/components/adguard/const.py + homeassistant/components/adguard/entity.py homeassistant/components/adguard/sensor.py homeassistant/components/adguard/switch.py homeassistant/components/ads/* @@ -136,6 +137,7 @@ omit = homeassistant/components/bosch_shc/switch.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py + homeassistant/components/braviatv/entity.py homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/remote.py homeassistant/components/broadlink/__init__.py @@ -210,7 +212,6 @@ omit = homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py - homeassistant/components/devolo_home_control/light.py homeassistant/components/devolo_home_control/sensor.py homeassistant/components/devolo_home_control/switch.py homeassistant/components/digital_ocean/* @@ -267,6 +268,7 @@ omit = homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/__init__.py homeassistant/components/elkm1/alarm_control_panel.py + homeassistant/components/elkm1/binary_sensor.py homeassistant/components/elkm1/climate.py homeassistant/components/elkm1/discovery.py homeassistant/components/elkm1/light.py @@ -276,6 +278,7 @@ omit = homeassistant/components/elmax/__init__.py homeassistant/components/elmax/common.py homeassistant/components/elmax/const.py + homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/switch.py homeassistant/components/elv/* homeassistant/components/emby/media_player.py @@ -439,7 +442,6 @@ omit = homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_pubsub/__init__.py - homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py @@ -449,6 +451,7 @@ omit = homeassistant/components/gtfs/sensor.py homeassistant/components/guardian/__init__.py homeassistant/components/guardian/binary_sensor.py + homeassistant/components/guardian/button.py homeassistant/components/guardian/sensor.py homeassistant/components/guardian/switch.py homeassistant/components/guardian/util.py @@ -555,6 +558,7 @@ omit = homeassistant/components/insteon/utils.py homeassistant/components/intellifire/__init__.py homeassistant/components/intellifire/coordinator.py + homeassistant/components/intellifire/climate.py homeassistant/components/intellifire/binary_sensor.py homeassistant/components/intellifire/sensor.py homeassistant/components/intellifire/switch.py @@ -642,9 +646,6 @@ omit = homeassistant/components/life360/const.py homeassistant/components/life360/coordinator.py homeassistant/components/life360/device_tracker.py - homeassistant/components/lifx/__init__.py - homeassistant/components/lifx/const.py - homeassistant/components/lifx/light.py homeassistant/components/lifx_cloud/scene.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py @@ -716,7 +717,6 @@ omit = homeassistant/components/microsoft/tts.py homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/hub.py - homeassistant/components/mikrotik/device_tracker.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py homeassistant/components/mill/sensor.py @@ -927,7 +927,6 @@ omit = homeassistant/components/plex/cast.py homeassistant/components/plex/media_player.py homeassistant/components/plex/view.py - homeassistant/components/plugwise/select.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/__init__.py @@ -1109,7 +1108,6 @@ omit = homeassistant/components/smtp/notify.py homeassistant/components/snapcast/* homeassistant/components/snmp/* - homeassistant/components/sochain/sensor.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge/sensor.py @@ -1494,6 +1492,7 @@ omit = homeassistant/components/yolink/climate.py homeassistant/components/yolink/const.py homeassistant/components/yolink/coordinator.py + homeassistant/components/yolink/cover.py homeassistant/components/yolink/entity.py homeassistant/components/yolink/lock.py homeassistant/components/yolink/sensor.py @@ -1521,7 +1520,6 @@ omit = homeassistant/components/zha/light.py homeassistant/components/zha/sensor.py homeassistant/components/zhong_hong/climate.py - homeassistant/components/xbee/* homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* homeassistant/components/supla/* diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 92de30ffe5a..52d25226930 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -33,6 +33,7 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New integration (thank you!) - [ ] New feature (which adds functionality to an existing integration) +- [ ] Deprecation (breaking change to happen in the future) - [ ] Breaking change (fix/feature causing existing functionality to break) - [ ] Code quality improvements to existing code or addition of tests diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 6eea7cea953..ac30becb128 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -102,9 +102,20 @@ jobs: - name: Checkout the repository uses: actions/checkout@v3.0.2 + - name: Download nightly wheels of frontend + if: needs.init.outputs.channel == 'dev' + uses: dawidd6/action-download-artifact@v2 + with: + github_token: ${{secrets.GITHUB_TOKEN}} + repo: home-assistant/frontend + branch: dev + workflow: nightly.yaml + workflow_conclusion: success + name: wheels + - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -112,10 +123,23 @@ jobs: if: needs.init.outputs.channel == 'dev' shell: bash run: | - python3 -m pip install packaging + python3 -m pip install packaging tomli python3 -m pip install --use-deprecated=legacy-resolver . version="$(python3 script/version_bump.py nightly)" + if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then + echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}" + frontend_version="${BASH_REMATCH[1]}" yq \ + --inplace e -o json \ + '.requirements = ["home-assistant-frontend=="+env(frontend_version)]' \ + homeassistant/components/frontend/manifest.json + + sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \ + homeassistant/package_constraints.txt + + python -m script.gen_requirements_all + fi + - name: Write meta info file shell: bash run: | @@ -135,7 +159,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.06.2 + uses: home-assistant/builder@2022.07.0 with: args: | $BUILD_ARGS \ @@ -201,7 +225,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.06.2 + uses: home-assistant/builder@2022.07.0 with: args: | $BUILD_ARGS \ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bf690740c6d..24d37b94518 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,9 +20,9 @@ on: type: boolean env: - CACHE_VERSION: 10 - PIP_CACHE_VERSION: 4 - HA_SHORT_VERSION: 2022.7 + CACHE_VERSION: 1 + PIP_CACHE_VERSION: 1 + HA_SHORT_VERSION: 2022.8 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache @@ -35,24 +35,38 @@ concurrency: cancel-in-progress: true jobs: - changes: - name: Determine what has changed + info: + name: Collect information & changes data outputs: # In case of issues with the partial run, use the following line instead: # test_full_suite: 'true' - test_full_suite: ${{ steps.info.outputs.test_full_suite }} core: ${{ steps.core.outputs.changes }} - integrations: ${{ steps.integrations.outputs.changes }} integrations_glob: ${{ steps.info.outputs.integrations_glob }} - tests: ${{ steps.info.outputs.tests }} - tests_glob: ${{ steps.info.outputs.tests_glob }} - test_groups: ${{ steps.info.outputs.test_groups }} - test_group_count: ${{ steps.info.outputs.test_group_count }} + integrations: ${{ steps.integrations.outputs.changes }} + pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }} + python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }} requirements: ${{ steps.core.outputs.requirements }} - runs-on: ubuntu-latest + test_full_suite: ${{ steps.info.outputs.test_full_suite }} + test_group_count: ${{ steps.info.outputs.test_group_count }} + test_groups: ${{ steps.info.outputs.test_groups }} + tests_glob: ${{ steps.info.outputs.tests_glob }} + tests: ${{ steps.info.outputs.tests }} + runs-on: ubuntu-20.04 steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 + - name: Generate partial Python venv restore key + id: generate_python_cache_key + run: >- + echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_all.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }}" + - name: Generate partial pre-commit restore key + id: generate_pre-commit_cache_key + run: >- + echo "::set-output name=key::${{ env.CACHE_VERSION }}-${{ env.DEFAULT_PYTHON }}-${{ + hashFiles('.pre-commit-config.yaml') }}" - name: Filter for core changes uses: dorny/paths-filter@v2.10.2 id: core @@ -79,8 +93,8 @@ jobs: # Defaults integrations_glob="" test_full_suite="true" - test_groups="[1, 2, 3, 4, 5, 6]" - test_group_count=6 + test_groups="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" + test_group_count=10 tests="[]" tests_glob="" @@ -123,8 +137,8 @@ jobs: || [[ "${{ github.event.inputs.full }}" == "true" ]] \ || [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-full-run') }}" == "true" ]]; then - test_groups="[1, 2, 3, 4, 5, 6]" - test_group_count=6 + test_groups="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" + test_group_count=10 test_full_suite="true" fi @@ -142,84 +156,39 @@ jobs: echo "tests_glob: ${tests_glob}" echo "::set-output name=tests_glob::${tests_glob}" - # Separate job to pre-populate the base dependency cache - # This prevent upcoming jobs to do the same individually - prepare-base: - name: Prepare base dependencies - runs-on: ubuntu-latest - timeout-minutes: 20 - outputs: - python-key: ${{ steps.generate-python-key.outputs.key }} - pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} + pre-commit: + name: Prepare pre-commit base + runs-on: ubuntu-20.04 + needs: + - info steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }}" - - name: Generate partial pip restore key - id: generate-pip-key - run: >- - echo "::set-output name=key::base-pip-${{ env.PIP_CACHE_VERSION }}-${{ - env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" + cache: "pip" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-python-key.outputs.key }} - # Temporary disabling the restore of environments when bumping - # a dependency. It seems that we are experiencing issues with - # restoring environments in GitHub Actions, although unclear why. - # First attempt: https://github.com/home-assistant/core/pull/62383 - # - # restore-keys: | - # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- - # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- - # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - - name: Restore pip wheel cache - if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.4 - with: - path: ${{ env.PIP_CACHE }} - key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-pip-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel - pip install --cache-dir=$PIP_CACHE -r requirements.txt -r requirements_test.txt --use-deprecated=legacy-resolver - - name: Generate partial pre-commit restore key - id: generate-pre-commit-key - run: >- - echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ - hashFiles('.pre-commit-config.yaml') }}" + pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: >- - ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}- + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -228,25 +197,24 @@ jobs: lint-black: name: Check black - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-base + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -254,49 +222,48 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | echo "Failed to restore pre-commit environment from cache" exit 1 - name: Run black (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual black --all-files --show-diff-on-failure - name: Run black (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure + pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* --show-diff-on-failure lint-flake8: name: Check flake8 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-base + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -304,10 +271,10 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -317,37 +284,38 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/flake8.json" - name: Run flake8 (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual flake8 --all-files - name: Run flake8 (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* + pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* lint-isort: name: Check isort - runs-on: ubuntu-latest - needs: prepare-base + runs-on: ubuntu-20.04 + needs: + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -355,10 +323,10 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -371,25 +339,24 @@ jobs: lint-other: name: Check other linters - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-base + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -397,10 +364,10 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -408,17 +375,17 @@ jobs: exit 1 - name: Run pyupgrade (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure - name: Run pyupgrade (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual pyupgrade --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure + pre-commit run --hook-stage manual pyupgrade --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* --show-diff-on-failure - name: Register yamllint problem matcher run: | @@ -437,17 +404,17 @@ jobs: pre-commit run --hook-stage manual check-json --all-files - name: Run prettier (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual prettier --all-files - name: Run prettier (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate - pre-commit run --hook-stage manual prettier --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* + pre-commit run --hook-stage manual prettier --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* - name: Register check executables problem matcher run: | @@ -478,36 +445,105 @@ jobs: args: hadolint Dockerfile.dev - name: Run bandit (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure - name: Run bandit (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure + pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* --show-diff-on-failure - hassfest: - name: Check hassfest - runs-on: ubuntu-latest - needs: prepare-tests + base: + name: Prepare dependencies + runs-on: ubuntu-20.04 + needs: info + timeout-minutes: 60 strategy: matrix: - python-version: [3.9] - container: homeassistant/ci-azure:${{ matrix.python-version }} + python-version: ["3.9", "3.10"] steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ matrix.python-version }} + - name: Generate partial pip restore key + id: generate-pip-key + run: >- + echo "::set-output name=key::pip-${{ env.PIP_CACHE_VERSION }}-${{ + env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" + - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} + - name: Restore pip wheel cache + if: steps.cache-venv.outputs.cache-hit != 'true' + uses: actions/cache@v3.0.5 + with: + path: ${{ env.PIP_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-pip-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + - name: Install additional OS dependencies + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg \ + libavcodec-dev \ + libavdevice-dev \ + libavfilter-dev \ + libavformat-dev \ + libavutil-dev \ + libswresample-dev \ + libswscale-dev \ + libudev-dev + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python --version + pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.3" setuptools wheel + pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver + pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver + pip install -e . + + hassfest: + name: Check hassfest + runs-on: ubuntu-20.04 + needs: + - info + - base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.0.2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment + id: cache-venv + uses: actions/cache@v3.0.5 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -520,23 +556,26 @@ jobs: gen-requirements-all: name: Check all requirements - runs-on: ubuntu-latest - needs: prepare-base + runs-on: ubuntu-20.04 + needs: + - info + - base steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 id: python + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -547,94 +586,29 @@ jobs: . venv/bin/activate python -m script.gen_requirements_all validate - prepare-tests: - name: Prepare tests for Python ${{ matrix.python-version }} - runs-on: ubuntu-latest - timeout-minutes: 60 - strategy: - matrix: - python-version: ["3.9", "3.10"] - outputs: - python-key: ${{ steps.generate-python-key.outputs.key }} - container: homeassistant/ci-azure:${{ matrix.python-version }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }}" - - name: Generate partial pip restore key - id: generate-pip-key - run: >- - echo "::set-output name=key::pip-${{ env.PIP_CACHE_VERSION }}-${{ - env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache@v3.0.4 - with: - path: venv - key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ - steps.generate-python-key.outputs.key }} - # Temporary disabling the restore of environments when bumping - # a dependency. It seems that we are experiencing issues with - # restoring environments in GitHub Actions, although unclear why. - # First attempt: https://github.com/home-assistant/core/pull/62383 - # - # restore-keys: | - # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- - # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- - # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - - name: Restore pip wheel cache - if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.4 - with: - path: ${{ env.PIP_CACHE }} - key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ - steps.generate-pip-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - - name: Create full Python ${{ matrix.python-version }} virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - # Temporary addition of cmake, needed to build some Python 3.9 packages - apt-get update - apt-get -y install cmake - - python -m venv venv - . venv/bin/activate - python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel - pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver - pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver - pip install -e . - pylint: name: Check pylint - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 timeout-minutes: 20 needs: - - changes - - prepare-tests - strategy: - matrix: - python-version: [3.9] - container: homeassistant/ci-azure:${{ matrix.python-version }} + - info + - base steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -644,39 +618,41 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pylint.json" - name: Run pylint (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate python --version - pylint homeassistant + pylint --ignore-missing-annotations=y homeassistant - name: Run pylint (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate python --version - pylint homeassistant/components/${{ needs.changes.outputs.integrations_glob }} + pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }} mypy: name: Check mypy - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-tests - strategy: - matrix: - python-version: [3.9] - container: homeassistant/ci-azure:${{ matrix.python-version }} + - info + - base steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -686,41 +662,45 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/mypy.json" - name: Run mypy (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate python --version mypy homeassistant pylint - name: Run mypy (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate python --version - mypy homeassistant/components/${{ needs.changes.outputs.integrations_glob }} + mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }} pip-check: - runs-on: ubuntu-latest - if: needs.changes.outputs.requirements == 'true' || github.event.inputs.full == 'true' + runs-on: ubuntu-20.04 needs: - - changes - - prepare-tests + - info + - base strategy: fail-fast: false matrix: - python-version: [3.9] + python-version: ["3.9", "3.10"] name: Run pip check ${{ matrix.python-version }} - container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -732,38 +712,48 @@ jobs: ./script/pip_check $PIP_CACHE pytest: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' - && (needs.changes.outputs.test_full_suite == 'true' || needs.changes.outputs.tests_glob) + && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob) needs: - - changes + - info + - base - gen-requirements-all - hassfest - lint-black - lint-other - lint-isort - mypy - - prepare-tests strategy: fail-fast: false matrix: - group: ${{ fromJson(needs.changes.outputs.test_groups) }} + group: ${{ fromJson(needs.info.outputs.test_groups) }} python-version: ["3.9", "3.10"] name: >- Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) - container: homeassistant/ci-azure:${{ matrix.python-version }} steps: + - name: Install additional OS dependencies + run: | + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg - name: Check out code from GitHub uses: actions/checkout@v3.0.2 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -783,7 +773,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Run pytest (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' timeout-minutes: 60 run: | . venv/bin/activate @@ -794,7 +784,7 @@ jobs: --durations=10 \ -n auto \ --dist=loadfile \ - --test-group-count ${{ needs.changes.outputs.test_group_count }} \ + --test-group-count ${{ needs.info.outputs.test_group_count }} \ --test-group=${{ matrix.group }} \ --cov="homeassistant" \ --cov-report=xml \ @@ -802,8 +792,8 @@ jobs: -p no:sugar \ tests - name: Run pytest (partially) - if: needs.changes.outputs.test_full_suite == 'false' - timeout-minutes: 20 + if: needs.info.outputs.test_full_suite == 'false' + timeout-minutes: 10 shell: bash run: | . venv/bin/activate @@ -838,9 +828,9 @@ jobs: coverage: name: Upload test coverage to Codecov - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes + - info - pytest steps: - name: Check out code from GitHub @@ -848,10 +838,10 @@ jobs: - name: Download all coverage artifacts uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' uses: codecov/codecov-action@v3.1.0 with: flags: full-suite - name: Upload coverage to Codecov (partial coverage) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' uses: codecov/codecov-action@v3.1.0 diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 0d3ddc4ca18..bc9fa63c86c 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 95f1d8e437e..2ffc2f1f721 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -157,6 +157,9 @@ jobs: echo "cmake==3.22.2" ) >> homeassistant/package_constraints.txt + # Do not pin numpy in wheels building + sed -i "/numpy/d" homeassistant/package_constraints.txt + - name: Build wheels uses: home-assistant/wheels@2022.06.7 with: diff --git a/.ignore b/.ignore deleted file mode 100644 index 45c6dc5561f..00000000000 --- a/.ignore +++ /dev/null @@ -1,6 +0,0 @@ -# Patterns matched in this file will be ignored by supported search utilities - -# Ignore generated html and javascript files -/homeassistant/components/frontend/www_static/*.html -/homeassistant/components/frontend/www_static/*.js -/homeassistant/components/frontend/www_static/panels/*.html diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18b34a222aa..8e259c1d063 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.2 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black args: @@ -21,7 +21,7 @@ repos: - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] - exclude: ^tests/fixtures/ + exclude: ^tests/fixtures/|homeassistant/generated/ - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 hooks: @@ -31,8 +31,8 @@ repos: - pyflakes==2.4.0 - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - - flake8-comprehensions==3.8.0 - - flake8-noqa==1.2.1 + - flake8-comprehensions==3.10.0 + - flake8-noqa==1.2.5 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit @@ -61,7 +61,7 @@ repos: - --branch=master - --branch=rc - repo: https://github.com/adrienverge/yamllint.git - rev: v1.26.3 + rev: v1.27.1 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier @@ -96,7 +96,7 @@ repos: files: ^(homeassistant|pylint)/.+\.py$ - id: pylint name: pylint - entry: script/run-in-env.sh pylint -j 0 + entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y language: script types: [python] files: ^homeassistant/.+\.py$ diff --git a/.strict-typing b/.strict-typing index 1832a83641a..c5b4e376414 100644 --- a/.strict-typing +++ b/.strict-typing @@ -15,12 +15,17 @@ homeassistant.auth.auth_store homeassistant.auth.providers.* homeassistant.helpers.area_registry homeassistant.helpers.condition +homeassistant.helpers.debounce +homeassistant.helpers.deprecation homeassistant.helpers.discovery +homeassistant.helpers.dispatcher homeassistant.helpers.entity +homeassistant.helpers.entity_platform homeassistant.helpers.entity_values homeassistant.helpers.event homeassistant.helpers.reload homeassistant.helpers.script_variables +homeassistant.helpers.singleton homeassistant.helpers.sun homeassistant.helpers.translation homeassistant.util.async_ @@ -57,6 +62,7 @@ homeassistant.components.automation.* homeassistant.components.backup.* homeassistant.components.baf.* homeassistant.components.binary_sensor.* +homeassistant.components.bluetooth.* homeassistant.components.bluetooth_tracker.* homeassistant.components.bmw_connected_drive.* homeassistant.components.bond.* @@ -109,6 +115,7 @@ homeassistant.components.group.* homeassistant.components.guardian.* homeassistant.components.history.* homeassistant.components.homeassistant.triggers.event +homeassistant.components.homeassistant_alerts.* homeassistant.components.homekit homeassistant.components.homekit.accessories homeassistant.components.homekit.aidmanager @@ -145,6 +152,8 @@ homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* homeassistant.components.light.* +homeassistant.components.lifx.* +homeassistant.components.litterrobot.* homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.logbook.* @@ -153,6 +162,7 @@ homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.media_source.* +homeassistant.components.metoffice.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* @@ -190,6 +200,8 @@ homeassistant.components.recollect_waste.* homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* +homeassistant.components.repairs.* +homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* diff --git a/.vscode/launch.json b/.vscode/launch.json index e8bf893e0c9..3cec89cc7e6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,14 @@ "justMyCode": false, "args": ["--debug", "-c", "config"] }, + { + "name": "Home Assistant (skip pip)", + "type": "python", + "request": "launch", + "module": "homeassistant", + "justMyCode": false, + "args": ["--debug", "-c", "config", "--skip-pip"] + }, { // Debug by attaching to local Home Asistant server using Remote Python Debugger. // See https://www.home-assistant.io/integrations/debugpy/ diff --git a/CODEOWNERS b/CODEOWNERS index 572ffc11081..5853186d0bb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -74,6 +74,8 @@ build.json @home-assistant/supervisor /tests/components/analytics/ @home-assistant/core @ludeeus /homeassistant/components/androidtv/ @JeffLIrion @ollo69 /tests/components/androidtv/ @JeffLIrion @ollo69 +/homeassistant/components/anthemav/ @hyralex +/tests/components/anthemav/ @hyralex /homeassistant/components/apache_kafka/ @bachya /tests/components/apache_kafka/ @bachya /homeassistant/components/api/ @home-assistant/core @@ -136,6 +138,8 @@ build.json @home-assistant/supervisor /homeassistant/components/blueprint/ @home-assistant/core /tests/components/blueprint/ @home-assistant/core /homeassistant/components/bluesound/ @thrawnarn +/homeassistant/components/bluetooth/ @bdraco +/tests/components/bluetooth/ @bdraco /homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe /tests/components/bmw_connected_drive/ @gerard33 @rikroe /homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto @@ -407,6 +411,8 @@ build.json @home-assistant/supervisor /homeassistant/components/google_cloud/ @lufton /homeassistant/components/google_travel_time/ @eifinger /tests/components/google_travel_time/ @eifinger +/homeassistant/components/govee_ble/ @bdraco +/tests/components/govee_ble/ @bdraco /homeassistant/components/gpsd/ @fabaff /homeassistant/components/gree/ @cmroche /tests/components/gree/ @cmroche @@ -449,6 +455,8 @@ build.json @home-assistant/supervisor /tests/components/home_plus_control/ @chemaaa /homeassistant/components/homeassistant/ @home-assistant/core /tests/components/homeassistant/ @home-assistant/core +/homeassistant/components/homeassistant_alerts/ @home-assistant/core +/tests/components/homeassistant_alerts/ @home-assistant/core /homeassistant/components/homeassistant_yellow/ @home-assistant/core /tests/components/homeassistant_yellow/ @home-assistant/core /homeassistant/components/homekit/ @bdraco @@ -494,6 +502,8 @@ build.json @home-assistant/supervisor /homeassistant/components/incomfort/ @zxdavb /homeassistant/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 +/homeassistant/components/inkbird/ @bdraco +/tests/components/inkbird/ @bdraco /homeassistant/components/input_boolean/ @home-assistant/core /tests/components/input_boolean/ @home-assistant/core /homeassistant/components/input_button/ @home-assistant/core @@ -573,7 +583,8 @@ build.json @home-assistant/supervisor /homeassistant/components/lg_netcast/ @Drafteed /homeassistant/components/life360/ @pnbruckner /tests/components/life360/ @pnbruckner -/homeassistant/components/lifx/ @Djelibeybi +/homeassistant/components/lifx/ @bdraco @Djelibeybi +/tests/components/lifx/ @bdraco @Djelibeybi /homeassistant/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core /homeassistant/components/linux_battery/ @fabaff @@ -628,8 +639,8 @@ build.json @home-assistant/supervisor /homeassistant/components/meteoalarm/ @rolfberkenbosch /homeassistant/components/meteoclimatic/ @adrianmo /tests/components/meteoclimatic/ @adrianmo -/homeassistant/components/metoffice/ @MrHarcombe -/tests/components/metoffice/ @MrHarcombe +/homeassistant/components/metoffice/ @MrHarcombe @avee87 +/tests/components/metoffice/ @MrHarcombe @avee87 /homeassistant/components/miflora/ @danielhiversen @basnijholt /homeassistant/components/mikrotik/ @engrbm87 /tests/components/mikrotik/ @engrbm87 @@ -641,6 +652,8 @@ build.json @home-assistant/supervisor /tests/components/minecraft_server/ @elmurato /homeassistant/components/minio/ @tkislan /tests/components/minio/ @tkislan +/homeassistant/components/moat/ @bdraco +/tests/components/moat/ @bdraco /homeassistant/components/mobile_app/ @home-assistant/core /tests/components/mobile_app/ @home-assistant/core /homeassistant/components/modbus/ @adamchengtkc @janiversen @vzahradnik @@ -696,6 +709,8 @@ build.json @home-assistant/supervisor /homeassistant/components/nextbus/ @vividboarder /tests/components/nextbus/ @vividboarder /homeassistant/components/nextcloud/ @meichthys +/homeassistant/components/nextdns/ @bieniu +/tests/components/nextdns/ @bieniu /homeassistant/components/nfandroidtv/ @tkdrob /tests/components/nfandroidtv/ @tkdrob /homeassistant/components/nightscout/ @marciogranzotto @@ -852,11 +867,15 @@ build.json @home-assistant/supervisor /tests/components/remote/ @home-assistant/core /homeassistant/components/renault/ @epenet /tests/components/renault/ @epenet +/homeassistant/components/repairs/ @home-assistant/core +/tests/components/repairs/ @home-assistant/core /homeassistant/components/repetier/ @MTrab @ShadowBr0ther /homeassistant/components/rflink/ @javicalle /tests/components/rflink/ @javicalle /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 /tests/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 +/homeassistant/components/rhasspy/ @balloob @synesthesiam +/tests/components/rhasspy/ @balloob @synesthesiam /homeassistant/components/ridwell/ @bachya /tests/components/ridwell/ @bachya /homeassistant/components/ring/ @balloob @@ -911,6 +930,8 @@ build.json @home-assistant/supervisor /tests/components/sensibo/ @andrey-git @gjohansson-ST /homeassistant/components/sensor/ @home-assistant/core /tests/components/sensor/ @home-assistant/core +/homeassistant/components/sensorpush/ @bdraco +/tests/components/sensorpush/ @bdraco /homeassistant/components/sentry/ @dcramer @frenck /tests/components/sentry/ @dcramer @frenck /homeassistant/components/senz/ @milanmeu @@ -977,6 +998,8 @@ build.json @home-assistant/supervisor /tests/components/songpal/ @rytilahti @shenxn /homeassistant/components/sonos/ @cgtobi @jjlawren /tests/components/sonos/ @cgtobi @jjlawren +/homeassistant/components/soundtouch/ @kroimon +/tests/components/soundtouch/ @kroimon /homeassistant/components/spaceapi/ @fabaff /tests/components/spaceapi/ @fabaff /homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87 @@ -1021,11 +1044,11 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core -/homeassistant/components/switchbot/ @danielhiversen @RenierM26 -/tests/components/switchbot/ @danielhiversen @RenierM26 +/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston +/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode -/homeassistant/components/switchmate/ @danielhiversen +/homeassistant/components/switchmate/ @danielhiversen @qiz-li /homeassistant/components/syncthing/ @zhulik /tests/components/syncthing/ @zhulik /homeassistant/components/syncthru/ @nielstron @@ -1203,6 +1226,8 @@ build.json @home-assistant/supervisor /homeassistant/components/xbox_live/ @MartinHjelmare /homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi /tests/components/xiaomi_aqara/ @danielhiversen @syssi +/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79 +/tests/components/xiaomi_ble/ @Jc2k @Ernst79 /homeassistant/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu /tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu /homeassistant/components/xiaomi_tv/ @simse @@ -1226,8 +1251,8 @@ build.json @home-assistant/supervisor /tests/components/zeroconf/ @bdraco /homeassistant/components/zerproc/ @emlove /tests/components/zerproc/ @emlove -/homeassistant/components/zha/ @dmulcahey @adminiuga -/tests/components/zha/ @dmulcahey @adminiuga +/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly +/tests/components/zha/ @dmulcahey @adminiuga @puddly /homeassistant/components/zodiac/ @JulienTant /tests/components/zodiac/ @JulienTant /homeassistant/components/zone/ @home-assistant/core diff --git a/Dockerfile b/Dockerfile index 13552d55a3d..03bd9131ea0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,12 @@ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements.txt --use-deprecated=legacy-resolver -COPY requirements_all.txt homeassistant/ +COPY requirements_all.txt home_assistant_frontend-* homeassistant/ RUN \ - pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ + pip3 install --no-cache-dir --no-index homeassistant/home_assistant_frontend-*.whl; \ + fi \ + && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver ## Setup Home Assistant Core diff --git a/build.yaml b/build.yaml index 7bd4abcfc20..9cf66e2621a 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.06.2 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.06.2 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.06.2 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.06.2 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.06.2 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.07.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.07.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.07.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.07.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.07.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index baf5a8bf3b3..2597781dc60 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -46,7 +46,7 @@ class AuthStore: self._users: dict[str, models.User] | None = None self._groups: dict[str, models.Group] | None = None self._perm_lookup: PermissionLookup | None = None - self._store = Store( + self._store = Store[dict[str, list[dict[str, Any]]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._lock = asyncio.Lock() @@ -483,9 +483,10 @@ class AuthStore: jwt_key=rt_dict["jwt_key"], last_used_at=last_used_at, last_used_ip=rt_dict.get("last_used_ip"), - credential=credentials.get(rt_dict.get("credential_id")), version=rt_dict.get("version"), ) + if "credential_id" in rt_dict: + token.credential = credentials.get(rt_dict["credential_id"]) users[rt_dict["user_id"]].refresh_tokens[token.id] = token self._groups = groups diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 3872257a205..1de6c38aecf 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -7,7 +7,7 @@ from __future__ import annotations import asyncio from collections import OrderedDict import logging -from typing import Any +from typing import Any, cast import attr import voluptuous as vol @@ -100,7 +100,7 @@ class NotifyAuthModule(MultiFactorAuthModule): """Initialize the user data store.""" super().__init__(hass, config) self._user_settings: _UsersDict | None = None - self._user_store = Store( + self._user_store = Store[dict[str, dict[str, Any]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._include = config.get(CONF_INCLUDE, []) @@ -119,10 +119,8 @@ class NotifyAuthModule(MultiFactorAuthModule): if self._user_settings is not None: return - if (data := await self._user_store.async_load()) is None or not isinstance( - data, dict - ): - data = {STORAGE_USERS: {}} + if (data := await self._user_store.async_load()) is None: + data = cast(dict[str, dict[str, Any]], {STORAGE_USERS: {}}) self._user_settings = { user_id: NotifySetting(**setting) @@ -322,6 +320,7 @@ class NotifySetupFlow(SetupFlow): errors: dict[str, str] = {} hass = self._auth_module.hass + assert self._secret and self._count if user_input: verified = await hass.async_add_executor_job( _verify_otp, self._secret, user_input["code"], self._count @@ -336,7 +335,6 @@ class NotifySetupFlow(SetupFlow): errors["base"] = "invalid_code" # generate code every time, no retry logic - assert self._secret and self._count code = await hass.async_add_executor_job( _generate_otp, self._secret, self._count ) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index e503198f08b..397a7fcd386 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from io import BytesIO -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -77,7 +77,7 @@ class TotpAuthModule(MultiFactorAuthModule): """Initialize the user data store.""" super().__init__(hass, config) self._users: dict[str, str] | None = None - self._user_store = Store( + self._user_store = Store[dict[str, dict[str, str]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._init_lock = asyncio.Lock() @@ -93,16 +93,14 @@ class TotpAuthModule(MultiFactorAuthModule): if self._users is not None: return - if (data := await self._user_store.async_load()) is None or not isinstance( - data, dict - ): - data = {STORAGE_USERS: {}} + if (data := await self._user_store.async_load()) is None: + data = cast(dict[str, dict[str, str]], {STORAGE_USERS: {}}) self._users = data.get(STORAGE_USERS, {}) async def _async_save(self) -> None: """Save data.""" - await self._user_store.async_save({STORAGE_USERS: self._users}) + await self._user_store.async_save({STORAGE_USERS: self._users or {}}) def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str: """Create a ota_secret for user.""" diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index cb95907c9b2..d190a618596 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -61,10 +61,10 @@ class Data: def __init__(self, hass: HomeAssistant) -> None: """Initialize the user data store.""" self.hass = hass - self._store = Store( + self._store = Store[dict[str, list[dict[str, str]]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) - self._data: dict[str, Any] | None = None + self._data: dict[str, list[dict[str, str]]] | None = None # Legacy mode will allow usernames to start/end with whitespace # and will compare usernames case-insensitive. # Remove in 2020 or when we launch 1.0. @@ -80,10 +80,8 @@ class Data: async def async_load(self) -> None: """Load stored data.""" - if (data := await self._store.async_load()) is None or not isinstance( - data, dict - ): - data = {"users": []} + if (data := await self._store.async_load()) is None: + data = cast(dict[str, list[dict[str, str]]], {"users": []}) seen: set[str] = set() @@ -123,7 +121,8 @@ class Data: @property def users(self) -> list[dict[str, str]]: """Return users.""" - return self._data["users"] # type: ignore[index,no-any-return] + assert self._data is not None + return self._data["users"] def validate_login(self, username: str, password: str) -> None: """Validate a username and password. diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 9a96704a836..939d6e7669b 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -4,15 +4,15 @@ from __future__ import annotations from enum import Enum from typing import Any, TypeVar -_StrEnumT = TypeVar("_StrEnumT", bound="StrEnum") +_StrEnumSelfT = TypeVar("_StrEnumSelfT", bound="StrEnum") class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" def __new__( - cls: type[_StrEnumT], value: str, *args: Any, **kwargs: Any - ) -> _StrEnumT: + cls: type[_StrEnumSelfT], value: str, *args: Any, **kwargs: Any + ) -> _StrEnumSelfT: """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 81d0fa72134..d2858bfcdf1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,7 +24,7 @@ from .const import ( SIGNAL_BOOTSTRAP_INTEGRATONS, ) from .exceptions import HomeAssistantError -from .helpers import area_registry, device_registry, entity_registry +from .helpers import area_registry, device_registry, entity_registry, recorder from .helpers.dispatcher import async_dispatcher_send from .helpers.typing import ConfigType from .setup import ( @@ -35,7 +35,6 @@ from .setup import ( async_setup_component, ) from .util import dt as dt_util -from .util.async_ import gather_with_concurrency from .util.logging import async_activate_log_queue_handler from .util.package import async_get_user_site, is_virtual_env @@ -67,10 +66,19 @@ LOGGING_INTEGRATIONS = { # Error logging "system_log", "sentry", +} +FRONTEND_INTEGRATIONS = { + # Get the frontend up and running as soon as possible so problem + # integrations can be removed and database migration status is + # visible in frontend + "frontend", +} +RECORDER_INTEGRATIONS = { + # Setup after frontend # To record data "recorder", } -DISCOVERY_INTEGRATIONS = ("dhcp", "ssdp", "usb", "zeroconf") +DISCOVERY_INTEGRATIONS = ("bluetooth", "dhcp", "ssdp", "usb", "zeroconf") STAGE_1_INTEGRATIONS = { # We need to make sure discovery integrations # update their deps before stage 2 integrations @@ -84,10 +92,6 @@ STAGE_1_INTEGRATIONS = { "cloud", # Ensure supervisor is available "hassio", - # Get the frontend up and running as soon - # as possible so problem integrations can - # be removed - "frontend", } @@ -285,7 +289,9 @@ def async_enable_logging( This method must be run in the event loop. """ - fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" + fmt = ( + "%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) [%(name)s] %(message)s" + ) datefmt = "%Y-%m-%d %H:%M:%S" if not log_no_color: @@ -477,14 +483,9 @@ async def _async_set_up_integrations( integrations_to_process = [ int_or_exc - for int_or_exc in await gather_with_concurrency( - loader.MAX_LOAD_CONCURRENTLY, - *( - loader.async_get_integration(hass, domain) - for domain in old_to_resolve - ), - return_exceptions=True, - ) + for int_or_exc in ( + await loader.async_get_integrations(hass, old_to_resolve) + ).values() if isinstance(int_or_exc, loader.Integration) ] resolve_dependencies_tasks = [ @@ -508,11 +509,43 @@ async def _async_set_up_integrations( _LOGGER.info("Domains to be set up: %s", domains_to_setup) + def _cache_uname_processor() -> None: + """Cache the result of platform.uname().processor in the executor. + + Multiple modules call this function at startup which + executes a blocking subprocess call. This is a problem for the + asyncio event loop. By primeing the cache of uname we can + avoid the blocking call in the event loop. + """ + platform.uname().processor # pylint: disable=expression-not-assigned + + # Load the registries and cache the result of platform.uname().processor + await asyncio.gather( + device_registry.async_load(hass), + entity_registry.async_load(hass), + area_registry.async_load(hass), + hass.async_add_executor_job(_cache_uname_processor), + ) + + # Initialize recorder + if "recorder" in domains_to_setup: + recorder.async_initialize_recorder(hass) + # Load logging as soon as possible if logging_domains := domains_to_setup & LOGGING_INTEGRATIONS: _LOGGER.info("Setting up logging: %s", logging_domains) await async_setup_multi_components(hass, logging_domains, config) + # Setup frontend + if frontend_domains := domains_to_setup & FRONTEND_INTEGRATIONS: + _LOGGER.info("Setting up frontend: %s", frontend_domains) + await async_setup_multi_components(hass, frontend_domains, config) + + # Setup recorder + if recorder_domains := domains_to_setup & RECORDER_INTEGRATIONS: + _LOGGER.info("Setting up recorder: %s", recorder_domains) + await async_setup_multi_components(hass, recorder_domains, config) + # Start up debuggers. Start these first in case they want to wait. if debuggers := domains_to_setup & DEBUGGER_INTEGRATIONS: _LOGGER.debug("Setting up debuggers: %s", debuggers) @@ -522,7 +555,8 @@ async def _async_set_up_integrations( stage_1_domains: set[str] = set() # Find all dependencies of any dependency of any stage 1 integration that - # we plan on loading and promote them to stage 1 + # we plan on loading and promote them to stage 1. This is done only to not + # get misleading log messages deps_promotion: set[str] = STAGE_1_INTEGRATIONS while deps_promotion: old_deps_promotion = deps_promotion @@ -539,24 +573,13 @@ async def _async_set_up_integrations( deps_promotion.update(dep_itg.all_dependencies) - stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains - - def _cache_uname_processor() -> None: - """Cache the result of platform.uname().processor in the executor. - - Multiple modules call this function at startup which - executes a blocking subprocess call. This is a problem for the - asyncio event loop. By primeing the cache of uname we can - avoid the blocking call in the event loop. - """ - platform.uname().processor # pylint: disable=expression-not-assigned - - # Load the registries - await asyncio.gather( - device_registry.async_load(hass), - entity_registry.async_load(hass), - area_registry.async_load(hass), - hass.async_add_executor_job(_cache_uname_processor), + stage_2_domains = ( + domains_to_setup + - logging_domains + - frontend_domains + - recorder_domains + - debuggers + - stage_1_domains ) # Start setup diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index edd799d31f7..bd66aa66c9c 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = AbodeSystem(abode, polling) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await setup_hass_events(hass) await hass.async_add_executor_job(setup_hass_services, hass) diff --git a/homeassistant/components/abode/translations/ja.json b/homeassistant/components/abode/translations/ja.json index cd498691f4b..1eb5f1cc499 100644 --- a/homeassistant/components/abode/translations/ja.json +++ b/homeassistant/components/abode/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/abode/translations/pt.json b/homeassistant/components/abode/translations/pt.json index 95a51741222..3d6b007b471 100644 --- a/homeassistant/components/abode/translations/pt.json +++ b/homeassistant/components/abode/translations/pt.json @@ -18,7 +18,7 @@ "user": { "data": { "password": "Palavra-passe", - "username": "Endere\u00e7o de e-mail" + "username": "Email" } } } diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 70db587bd49..9123648a38d 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -11,12 +11,14 @@ from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN +from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -26,6 +28,7 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AccuWeather as config entry.""" api_key: str = entry.data[CONF_API_KEY] + name: str = entry.data[CONF_NAME] assert entry.unique_id is not None location_key = entry.unique_id forecast: bool = entry.options.get(CONF_FORECAST, False) @@ -35,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websession = async_get_clientsession(hass) coordinator = AccuWeatherDataUpdateCoordinator( - hass, websession, api_key, location_key, forecast + hass, websession, api_key, location_key, forecast, name ) await coordinator.async_config_entry_first_refresh() @@ -43,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -73,12 +76,27 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): api_key: str, location_key: str, forecast: bool, + name: str, ) -> None: """Initialize.""" self.location_key = location_key self.forecast = forecast self.is_metric = hass.config.units.is_metric - self.accuweather = AccuWeather(api_key, session, location_key=self.location_key) + self.accuweather = AccuWeather(api_key, session, location_key=location_key) + self.device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, location_key)}, + manufacturer=MANUFACTURER, + name=name, + # You don't need to provide specific details for the URL, + # so passing in _ characters is fine if the location key + # is correct + configuration_url=( + "http://accuweather.com/en/" + f"_/_/{location_key}/" + f"weather-forecast/{location_key}/" + ), + ) # Enabling the forecast download increases the number of requests per data # update, we use 40 minutes for current condition only and 80 minutes for diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 408d4700422..c1b90de09e7 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -20,22 +19,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ) -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_CUBIC_METER, - LENGTH_FEET, - LENGTH_INCHES, - LENGTH_METERS, - LENGTH_MILLIMETERS, - PERCENTAGE, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TIME_HOURS, - UV_INDEX, -) - -from .model import AccuWeatherSensorDescription API_IMPERIAL: Final = "Imperial" API_METRIC: Final = "Metric" @@ -45,7 +28,6 @@ CONF_FORECAST: Final = "forecast" DOMAIN: Final = "accuweather" MANUFACTURER: Final = "AccuWeather, Inc." MAX_FORECAST_DAYS: Final = 4 -NAME: Final = "AccuWeather" CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37], @@ -63,264 +45,3 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_SUNNY: [1, 2, 5], ATTR_CONDITION_WINDY: [32], } - -FORECAST_SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( - AccuWeatherSensorDescription( - key="CloudCoverDay", - icon="mdi:weather-cloudy", - name="Cloud Cover Day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="CloudCoverNight", - icon="mdi:weather-cloudy", - name="Cloud Cover Night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="Grass", - icon="mdi:grass", - name="Grass Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="HoursOfSun", - icon="mdi:weather-partly-cloudy", - name="Hours Of Sun", - unit_metric=TIME_HOURS, - unit_imperial=TIME_HOURS, - ), - AccuWeatherSensorDescription( - key="Mold", - icon="mdi:blur", - name="Mold Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="Ozone", - icon="mdi:vector-triangle", - name="Ozone", - unit_metric=None, - unit_imperial=None, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="Ragweed", - icon="mdi:sprout", - name="Ragweed Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureMax", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureMin", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShadeMax", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Shade Max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShadeMin", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Shade Min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="ThunderstormProbabilityDay", - icon="mdi:weather-lightning", - name="Thunderstorm Probability Day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - ), - AccuWeatherSensorDescription( - key="ThunderstormProbabilityNight", - icon="mdi:weather-lightning", - name="Thunderstorm Probability Night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - ), - AccuWeatherSensorDescription( - key="Tree", - icon="mdi:tree-outline", - name="Tree Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="UVIndex", - icon="mdi:weather-sunny", - name="UV Index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, - ), - AccuWeatherSensorDescription( - key="WindGustDay", - icon="mdi:weather-windy", - name="Wind Gust Day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="WindGustNight", - icon="mdi:weather-windy", - name="Wind Gust Night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="WindDay", - icon="mdi:weather-windy", - name="Wind Day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - ), - AccuWeatherSensorDescription( - key="WindNight", - icon="mdi:weather-windy", - name="Wind Night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - ), -) - -SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( - AccuWeatherSensorDescription( - key="ApparentTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="Apparent Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="Ceiling", - icon="mdi:weather-fog", - name="Cloud Ceiling", - unit_metric=LENGTH_METERS, - unit_imperial=LENGTH_FEET, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="CloudCover", - icon="mdi:weather-cloudy", - name="Cloud Cover", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="DewPoint", - device_class=SensorDeviceClass.TEMPERATURE, - name="Dew Point", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShade", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Shade", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="Precipitation", - icon="mdi:weather-rainy", - name="Precipitation", - unit_metric=LENGTH_MILLIMETERS, - unit_imperial=LENGTH_INCHES, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="PressureTendency", - device_class="accuweather__pressure_tendency", - icon="mdi:gauge", - name="Pressure Tendency", - unit_metric=None, - unit_imperial=None, - ), - AccuWeatherSensorDescription( - key="UVIndex", - icon="mdi:weather-sunny", - name="UV Index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="WetBulbTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="Wet Bulb Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="WindChillTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="Wind Chill Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="Wind", - icon="mdi:weather-windy", - name="Wind", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="WindGust", - icon="mdi:weather-windy", - name="Wind Gust", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), -) diff --git a/homeassistant/components/accuweather/model.py b/homeassistant/components/accuweather/model.py deleted file mode 100644 index e74a6d46057..00000000000 --- a/homeassistant/components/accuweather/model.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Type definitions for AccuWeather integration.""" -from __future__ import annotations - -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class AccuWeatherSensorDescription(SensorEntityDescription): - """Class describing AccuWeather sensor entities.""" - - unit_metric: str | None = None - unit_imperial: str | None = None diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 220575541ad..72182d4d635 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -1,14 +1,31 @@ """Support for the AccuWeather service.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any, cast -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_CUBIC_METER, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_METERS, + LENGTH_MILLIMETERS, + PERCENTAGE, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TIME_HOURS, + UV_INDEX, +) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -20,28 +37,292 @@ from .const import ( ATTR_FORECAST, ATTRIBUTION, DOMAIN, - FORECAST_SENSOR_TYPES, - MANUFACTURER, MAX_FORECAST_DAYS, - NAME, - SENSOR_TYPES, ) -from .model import AccuWeatherSensorDescription PARALLEL_UPDATES = 1 +@dataclass +class AccuWeatherSensorDescription(SensorEntityDescription): + """Class describing AccuWeather sensor entities.""" + + unit_metric: str | None = None + unit_imperial: str | None = None + + +FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( + AccuWeatherSensorDescription( + key="CloudCoverDay", + icon="mdi:weather-cloudy", + name="Cloud cover day", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="CloudCoverNight", + icon="mdi:weather-cloudy", + name="Cloud cover night", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Grass", + icon="mdi:grass", + name="Grass pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="HoursOfSun", + icon="mdi:weather-partly-cloudy", + name="Hours of sun", + unit_metric=TIME_HOURS, + unit_imperial=TIME_HOURS, + ), + AccuWeatherSensorDescription( + key="Mold", + icon="mdi:blur", + name="Mold pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Ozone", + icon="mdi:vector-triangle", + name="Ozone", + unit_metric=None, + unit_imperial=None, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Ragweed", + icon="mdi:sprout", + name="Ragweed pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureMax", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature max", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureMin", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature min", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMax", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature shade max", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMin", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature shade min", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="ThunderstormProbabilityDay", + icon="mdi:weather-lightning", + name="Thunderstorm probability day", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + ), + AccuWeatherSensorDescription( + key="ThunderstormProbabilityNight", + icon="mdi:weather-lightning", + name="Thunderstorm probability night", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + ), + AccuWeatherSensorDescription( + key="Tree", + icon="mdi:tree-outline", + name="Tree pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + name="UV index", + unit_metric=UV_INDEX, + unit_imperial=UV_INDEX, + ), + AccuWeatherSensorDescription( + key="WindGustDay", + icon="mdi:weather-windy", + name="Wind gust day", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="WindGustNight", + icon="mdi:weather-windy", + name="Wind gust night", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="WindDay", + icon="mdi:weather-windy", + name="Wind day", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + ), + AccuWeatherSensorDescription( + key="WindNight", + icon="mdi:weather-windy", + name="Wind night", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + ), +) + +SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( + AccuWeatherSensorDescription( + key="ApparentTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Apparent temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Ceiling", + icon="mdi:weather-fog", + name="Cloud ceiling", + unit_metric=LENGTH_METERS, + unit_imperial=LENGTH_FEET, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="CloudCover", + icon="mdi:weather-cloudy", + name="Cloud cover", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="DewPoint", + device_class=SensorDeviceClass.TEMPERATURE, + name="Dew point", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShade", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature shade", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Precipitation", + icon="mdi:weather-rainy", + name="Precipitation", + unit_metric=LENGTH_MILLIMETERS, + unit_imperial=LENGTH_INCHES, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="PressureTendency", + device_class="accuweather__pressure_tendency", + icon="mdi:gauge", + name="Pressure tendency", + unit_metric=None, + unit_imperial=None, + ), + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + name="UV index", + unit_metric=UV_INDEX, + unit_imperial=UV_INDEX, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WetBulbTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Wet bulb temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WindChillTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Wind chill temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Wind", + icon="mdi:weather-windy", + name="Wind", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WindGust", + icon="mdi:weather-windy", + name="Wind gust", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add AccuWeather entities from a config_entry.""" - name: str = entry.data[CONF_NAME] coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors: list[AccuWeatherSensor] = [] for description in SENSOR_TYPES: - sensors.append(AccuWeatherSensor(name, coordinator, description)) + sensors.append(AccuWeatherSensor(coordinator, description)) if coordinator.forecast: for description in FORECAST_SENSOR_TYPES: @@ -50,9 +331,7 @@ async def async_setup_entry( # locations. if description.key in coordinator.data[ATTR_FORECAST][0]: sensors.append( - AccuWeatherSensor( - name, coordinator, description, forecast_day=day - ) + AccuWeatherSensor(coordinator, description, forecast_day=day) ) async_add_entities(sensors) @@ -64,11 +343,11 @@ class AccuWeatherSensor( """Define an AccuWeather entity.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True entity_description: AccuWeatherSensorDescription def __init__( self, - name: str, coordinator: AccuWeatherDataUpdateCoordinator, description: AccuWeatherSensorDescription, forecast_day: int | None = None, @@ -81,12 +360,11 @@ class AccuWeatherSensor( ) self._attrs: dict[str, Any] = {} if forecast_day is not None: - self._attr_name = f"{name} {description.name} {forecast_day}d" + self._attr_name = f"{description.name} {forecast_day}d" self._attr_unique_id = ( f"{coordinator.location_key}-{description.key}-{forecast_day}".lower() ) else: - self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( f"{coordinator.location_key}-{description.key}".lower() ) @@ -96,12 +374,7 @@ class AccuWeatherSensor( else: self._unit_system = API_IMPERIAL self._attr_native_unit_of_measurement = description.unit_imperial - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, coordinator.location_key)}, - manufacturer=MANUFACTURER, - name=NAME, - ) + self._attr_device_info = coordinator.device_info self.forecast_day = forecast_day @property diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index 21ec4e6068d..b9c7819e78a 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { - "default": "\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u69cb\u6210\u5f8c\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u305d\u308c\u3089\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002" + "default": "\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u69cb\u6210\u5f8c\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u305d\u308c\u3089\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index 764218f8e11..4ec4d05a932 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -34,7 +34,7 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera AccuWeather", + "can_reach_server": "Dost\u0119p do serwera", "remaining_requests": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" } } diff --git a/homeassistant/components/accuweather/translations/pt.json b/homeassistant/components/accuweather/translations/pt.json index 08d419ec9b2..8b5d307e722 100644 --- a/homeassistant/components/accuweather/translations/pt.json +++ b/homeassistant/components/accuweather/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, + "create_entry": { + "default": "Alguns sensores n\u00e3o s\u00e3o ativados por defeito. Podem ser ativados no registo da entidade ap\u00f3s a configura\u00e7\u00e3o da integra\u00e7\u00e3o.\nA previs\u00e3o do tempo n\u00e3o est\u00e1 ativada por defeito. Pode ativ\u00e1-la nas op\u00e7\u00f5es de integra\u00e7\u00e3o." + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_api_key": "Chave de API inv\u00e1lida" diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index ae1824aef4a..e8a5b0ab396 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -18,7 +18,6 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_NAME, LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, @@ -31,8 +30,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utc_from_timestamp @@ -45,8 +42,6 @@ from .const import ( ATTRIBUTION, CONDITION_CLASSES, DOMAIN, - MANUFACTURER, - NAME, ) PARALLEL_UPDATES = 1 @@ -56,11 +51,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add a AccuWeather weather entity from a config_entry.""" - name: str = entry.data[CONF_NAME] coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([AccuWeatherEntity(name, coordinator)]) + async_add_entities([AccuWeatherEntity(coordinator)]) class AccuWeatherEntity( @@ -68,9 +62,9 @@ class AccuWeatherEntity( ): """Define an AccuWeather entity.""" - def __init__( - self, name: str, coordinator: AccuWeatherDataUpdateCoordinator - ) -> None: + _attr_has_entity_name = True + + def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None: """Initialize.""" super().__init__(coordinator) # Coordinator data is used also for sensors which don't have units automatically @@ -90,21 +84,9 @@ class AccuWeatherEntity( self._attr_native_temperature_unit = TEMP_FAHRENHEIT self._attr_native_visibility_unit = LENGTH_MILES self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR - self._attr_name = name self._attr_unique_id = coordinator.location_key self._attr_attribution = ATTRIBUTION - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, coordinator.location_key)}, - manufacturer=MANUFACTURER, - name=NAME, - # You don't need to provide specific details for the URL, - # so passing in _ characters is fine if the location key - # is correct - configuration_url="http://accuweather.com/en/" - f"_/_/{coordinator.location_key}/" - f"weather-forecast/{coordinator.location_key}/", - ) + self._attr_device_info = coordinator.device_info @property def condition(self) -> str | None: diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py index f9599a240e8..0bb7cbdc177 100644 --- a/homeassistant/components/acmeda/__init__.py +++ b/homeassistant/components/acmeda/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return False hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/adax/__init__.py b/homeassistant/components/adax/__init__.py index dc24e741c89..438f1fc74ad 100644 --- a/homeassistant/components/adax/__init__.py +++ b/homeassistant/components/adax/__init__.py @@ -10,7 +10,7 @@ PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Adax from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/adax/translations/pt.json b/homeassistant/components/adax/translations/pt.json new file mode 100644 index 00000000000..b83b23758af --- /dev/null +++ b/homeassistant/components/adax/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "cloud": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 2a244a5fe80..fbcbea61316 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,12 +1,10 @@ """Support for AdGuard Home.""" from __future__ import annotations -import logging - -from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError +from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol -from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -22,13 +20,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( CONF_FORCE, DATA_ADGUARD_CLIENT, - DATA_ADGUARD_VERSION, DOMAIN, SERVICE_ADD_URL, SERVICE_DISABLE_URL, @@ -37,8 +32,6 @@ from .const import ( SERVICE_REMOVE_URL, ) -_LOGGER = logging.getLogger(__name__) - SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url}) SERVICE_ADD_URL_SCHEMA = vol.Schema( {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.url} @@ -70,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def add_url(call: ServiceCall) -> None: """Service call to add a new filter subscription to AdGuard Home.""" @@ -127,91 +120,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN] return unload_ok - - -class AdGuardHomeEntity(Entity): - """Defines a base AdGuard Home entity.""" - - def __init__( - self, - adguard: AdGuardHome, - entry: ConfigEntry, - name: str, - icon: str, - enabled_default: bool = True, - ) -> None: - """Initialize the AdGuard Home entity.""" - self._available = True - self._enabled_default = enabled_default - self._icon = icon - self._name = name - self._entry = entry - self.adguard = adguard - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - async def async_update(self) -> None: - """Update AdGuard Home entity.""" - if not self.enabled: - return - - try: - await self._adguard_update() - self._available = True - except AdGuardHomeError: - if self._available: - _LOGGER.debug( - "An error occurred while updating AdGuard Home sensor", - exc_info=True, - ) - self._available = False - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - raise NotImplementedError() - - -class AdGuardHomeDeviceEntity(AdGuardHomeEntity): - """Defines a AdGuard Home device entity.""" - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this AdGuard Home instance.""" - if self._entry.source == SOURCE_HASSIO: - config_url = "homeassistant://hassio/ingress/a0d7b954_adguard" - else: - if self.adguard.tls: - config_url = f"https://{self.adguard.host}:{self.adguard.port}" - else: - config_url = f"http://{self.adguard.host}:{self.adguard.port}" - - return DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={ - (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] - }, - manufacturer="AdGuard Team", - name="AdGuard Home", - sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( - DATA_ADGUARD_VERSION - ), - configuration_url=config_url, - ) diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index 8bfa5b49fc6..a4ccde68539 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -1,7 +1,10 @@ """Constants for the AdGuard Home integration.""" +import logging DOMAIN = "adguard" +LOGGER = logging.getLogger(__package__) + DATA_ADGUARD_CLIENT = "adguard_client" DATA_ADGUARD_VERSION = "adguard_version" diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py new file mode 100644 index 00000000000..7d6bf099366 --- /dev/null +++ b/homeassistant/components/adguard/entity.py @@ -0,0 +1,69 @@ +"""AdGuard Home base entity.""" +from __future__ import annotations + +from adguardhome import AdGuardHome, AdGuardHomeError + +from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER + + +class AdGuardHomeEntity(Entity): + """Defines a base AdGuard Home entity.""" + + _attr_has_entity_name = True + _attr_available = True + + def __init__( + self, + adguard: AdGuardHome, + entry: ConfigEntry, + ) -> None: + """Initialize the AdGuard Home entity.""" + self._entry = entry + self.adguard = adguard + + async def async_update(self) -> None: + """Update AdGuard Home entity.""" + if not self.enabled: + return + + try: + await self._adguard_update() + self._attr_available = True + except AdGuardHomeError: + if self._attr_available: + LOGGER.debug( + "An error occurred while updating AdGuard Home sensor", + exc_info=True, + ) + self._attr_available = False + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + raise NotImplementedError() + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this AdGuard Home instance.""" + if self._entry.source == SOURCE_HASSIO: + config_url = "homeassistant://hassio/ingress/a0d7b954_adguard" + elif self.adguard.tls: + config_url = f"https://{self.adguard.host}:{self.adguard.port}" + else: + config_url = f"http://{self.adguard.host}:{self.adguard.port}" + + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={ + (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] + }, + manufacturer="AdGuard Team", + name="AdGuard Home", + sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( + DATA_ADGUARD_VERSION + ), + configuration_url=config_url, + ) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 8134d2c4d43..86104d15ef2 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -1,24 +1,102 @@ """Support for AdGuard Home sensors.""" from __future__ import annotations +from collections.abc import Callable, Coroutine +from dataclasses import dataclass from datetime import timedelta +from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AdGuardHomeDeviceEntity from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN +from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=300) PARALLEL_UPDATES = 4 +@dataclass +class AdGuardHomeEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[AdGuardHome], Coroutine[Any, Any, int | float]] + + +@dataclass +class AdGuardHomeEntityDescription( + SensorEntityDescription, AdGuardHomeEntityDescriptionMixin +): + """Describes AdGuard Home sensor entity.""" + + +SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( + AdGuardHomeEntityDescription( + key="dns_queries", + name="DNS queries", + icon="mdi:magnify", + native_unit_of_measurement="queries", + value_fn=lambda adguard: adguard.stats.dns_queries(), + ), + AdGuardHomeEntityDescription( + key="blocked_filtering", + name="DNS queries blocked", + icon="mdi:magnify-close", + native_unit_of_measurement="queries", + value_fn=lambda adguard: adguard.stats.blocked_filtering(), + ), + AdGuardHomeEntityDescription( + key="blocked_percentage", + name="DNS queries blocked ratio", + icon="mdi:magnify-close", + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda adguard: adguard.stats.blocked_percentage(), + ), + AdGuardHomeEntityDescription( + key="blocked_parental", + name="Parental control blocked", + icon="mdi:human-male-girl", + native_unit_of_measurement="requests", + value_fn=lambda adguard: adguard.stats.replaced_parental(), + ), + AdGuardHomeEntityDescription( + key="blocked_safebrowsing", + name="Safe browsing blocked", + icon="mdi:shield-half-full", + native_unit_of_measurement="requests", + value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(), + ), + AdGuardHomeEntityDescription( + key="enforced_safesearch", + name="Safe searches enforced", + icon="mdi:shield-search", + native_unit_of_measurement="requests", + value_fn=lambda adguard: adguard.stats.replaced_safesearch(), + ), + AdGuardHomeEntityDescription( + key="average_speed", + name="Average processing speed", + icon="mdi:speedometer", + native_unit_of_measurement=TIME_MILLISECONDS, + value_fn=lambda adguard: adguard.stats.avg_processing_time(), + ), + AdGuardHomeEntityDescription( + key="rules_count", + name="Rules count", + icon="mdi:counter", + native_unit_of_measurement="rules", + value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False), + entity_registry_enabled_default=False, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -34,215 +112,39 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version - sensors = [ - AdGuardHomeDNSQueriesSensor(adguard, entry), - AdGuardHomeBlockedFilteringSensor(adguard, entry), - AdGuardHomePercentageBlockedSensor(adguard, entry), - AdGuardHomeReplacedParentalSensor(adguard, entry), - AdGuardHomeReplacedSafeBrowsingSensor(adguard, entry), - AdGuardHomeReplacedSafeSearchSensor(adguard, entry), - AdGuardHomeAverageProcessingTimeSensor(adguard, entry), - AdGuardHomeRulesCountSensor(adguard, entry), - ] - - async_add_entities(sensors, True) + async_add_entities( + [AdGuardHomeSensor(adguard, entry, description) for description in SENSORS], + True, + ) -class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): +class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity): """Defines a AdGuard Home sensor.""" + entity_description: AdGuardHomeEntityDescription + def __init__( self, adguard: AdGuardHome, entry: ConfigEntry, - name: str, - icon: str, - measurement: str, - unit_of_measurement: str, - enabled_default: bool = True, + description: AdGuardHomeEntityDescription, ) -> None: """Initialize AdGuard Home sensor.""" - self._state: int | str | None = None - self._unit_of_measurement = unit_of_measurement - self.measurement = measurement - - super().__init__(adguard, entry, name, icon, enabled_default) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return "_".join( + super().__init__(adguard, entry) + self.entity_description = description + self._attr_unique_id = "_".join( [ DOMAIN, - self.adguard.host, - str(self.adguard.port), + adguard.host, + str(adguard.port), "sensor", - self.measurement, + description.key, ] ) - @property - def native_value(self) -> int | str | None: - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - -class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): - """Defines a AdGuard Home DNS Queries sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "AdGuard DNS Queries", - "mdi:magnify", - "dns_queries", - "queries", - ) - async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.dns_queries() - - -class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): - """Defines a AdGuard Home blocked by filtering sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "AdGuard DNS Queries Blocked", - "mdi:magnify-close", - "blocked_filtering", - "queries", - enabled_default=False, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.blocked_filtering() - - -class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): - """Defines a AdGuard Home blocked percentage sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "AdGuard DNS Queries Blocked Ratio", - "mdi:magnify-close", - "blocked_percentage", - PERCENTAGE, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - percentage = await self.adguard.stats.blocked_percentage() - self._state = f"{percentage:.2f}" - - -class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): - """Defines a AdGuard Home replaced by parental control sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "AdGuard Parental Control Blocked", - "mdi:human-male-girl", - "blocked_parental", - "requests", - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.replaced_parental() - - -class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): - """Defines a AdGuard Home replaced by safe browsing sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "AdGuard Safe Browsing Blocked", - "mdi:shield-half-full", - "blocked_safebrowsing", - "requests", - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.replaced_safebrowsing() - - -class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): - """Defines a AdGuard Home replaced by safe search sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "AdGuard Safe Searches Enforced", - "mdi:shield-search", - "enforced_safesearch", - "requests", - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.replaced_safesearch() - - -class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): - """Defines a AdGuard Home average processing time sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "AdGuard Average Processing Speed", - "mdi:speedometer", - "average_speed", - TIME_MILLISECONDS, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - average = await self.adguard.stats.avg_processing_time() - self._state = f"{average:.2f}" - - -class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): - """Defines a AdGuard Home rules count sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "AdGuard Rules Count", - "mdi:counter", - "rules_count", - "rules", - enabled_default=False, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.filtering.rules_count(allowlist=False) + value = await self.entity_description.value_fn(self.adguard) + self._attr_native_value = value + if isinstance(value, float): + self._attr_native_value = f"{value:.2f}" diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 0cb7a48fee6..a359bf86c2d 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -1,27 +1,94 @@ """Support for AdGuard Home switches.""" from __future__ import annotations +from collections.abc import Callable, Coroutine +from dataclasses import dataclass from datetime import timedelta -import logging from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AdGuardHomeDeviceEntity -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN, LOGGER +from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 1 +@dataclass +class AdGuardHomeSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + is_on_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, bool]]] + turn_on_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, None]]] + turn_off_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, None]]] + + +@dataclass +class AdGuardHomeSwitchEntityDescription( + SwitchEntityDescription, AdGuardHomeSwitchEntityDescriptionMixin +): + """Describes AdGuard Home switch entity.""" + + +SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = ( + AdGuardHomeSwitchEntityDescription( + key="protection", + name="Protection", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.protection_enabled, + turn_on_fn=lambda adguard: adguard.enable_protection, + turn_off_fn=lambda adguard: adguard.disable_protection, + ), + AdGuardHomeSwitchEntityDescription( + key="parental", + name="Parental control", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.parental.enabled, + turn_on_fn=lambda adguard: adguard.parental.enable, + turn_off_fn=lambda adguard: adguard.parental.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="safesearch", + name="Safe search", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.safesearch.enabled, + turn_on_fn=lambda adguard: adguard.safesearch.enable, + turn_off_fn=lambda adguard: adguard.safesearch.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="safebrowsing", + name="Safe browsing", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.safebrowsing.enabled, + turn_on_fn=lambda adguard: adguard.safebrowsing.enable, + turn_off_fn=lambda adguard: adguard.safebrowsing.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="filtering", + name="Filtering", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.filtering.enabled, + turn_on_fn=lambda adguard: adguard.filtering.enable, + turn_off_fn=lambda adguard: adguard.filtering.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="querylog", + name="Query log", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.querylog.enabled, + turn_on_fn=lambda adguard: adguard.querylog.enable, + turn_off_fn=lambda adguard: adguard.querylog.disable, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -37,203 +104,46 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version - switches = [ - AdGuardHomeProtectionSwitch(adguard, entry), - AdGuardHomeFilteringSwitch(adguard, entry), - AdGuardHomeParentalSwitch(adguard, entry), - AdGuardHomeSafeBrowsingSwitch(adguard, entry), - AdGuardHomeSafeSearchSwitch(adguard, entry), - AdGuardHomeQueryLogSwitch(adguard, entry), - ] - async_add_entities(switches, True) + async_add_entities( + [AdGuardHomeSwitch(adguard, entry, description) for description in SWITCHES], + True, + ) -class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): +class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity): """Defines a AdGuard Home switch.""" + entity_description: AdGuardHomeSwitchEntityDescription + def __init__( self, adguard: AdGuardHome, entry: ConfigEntry, - name: str, - icon: str, - key: str, - enabled_default: bool = True, + description: AdGuardHomeSwitchEntityDescription, ) -> None: """Initialize AdGuard Home switch.""" - self._state = False - self._key = key - super().__init__(adguard, entry, name, icon, enabled_default) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return "_".join( - [DOMAIN, self.adguard.host, str(self.adguard.port), "switch", self._key] + super().__init__(adguard, entry) + self.entity_description = description + self._attr_unique_id = "_".join( + [DOMAIN, adguard.host, str(adguard.port), "switch", description.key] ) - @property - def is_on(self) -> bool: - """Return the state of the switch.""" - return self._state - async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" try: - await self._adguard_turn_off() + await self.entity_description.turn_off_fn(self.adguard)() except AdGuardHomeError: - _LOGGER.error("An error occurred while turning off AdGuard Home switch") - self._available = False - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - raise NotImplementedError() + LOGGER.error("An error occurred while turning off AdGuard Home switch") + self._attr_available = False async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" try: - await self._adguard_turn_on() + await self.entity_description.turn_on_fn(self.adguard)() except AdGuardHomeError: - _LOGGER.error("An error occurred while turning on AdGuard Home switch") - self._available = False - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - raise NotImplementedError() - - -class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home protection switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "AdGuard Protection", "mdi:shield-check", "protection" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.disable_protection() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.enable_protection() + LOGGER.error("An error occurred while turning on AdGuard Home switch") + self._attr_available = False async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - self._state = await self.adguard.protection_enabled() - - -class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home parental control switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "AdGuard Parental Control", "mdi:shield-check", "parental" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.parental.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.parental.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.parental.enabled() - - -class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home safe search switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "AdGuard Safe Search", "mdi:shield-check", "safesearch" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.safesearch.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.safesearch.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.safesearch.enabled() - - -class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home safe search switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.safebrowsing.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.safebrowsing.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.safebrowsing.enabled() - - -class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home filtering switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "AdGuard Filtering", "mdi:shield-check", "filtering" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.filtering.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.filtering.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.filtering.enabled() - - -class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home query log switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, - entry, - "AdGuard Query Log", - "mdi:shield-check", - "querylog", - enabled_default=False, - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.querylog.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.querylog.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.querylog.enabled() + self._attr_is_on = await self.entity_description.is_on_fn(self.adguard)() diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index ffbf454b62b..59f33efee29 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "existing_instance_updated": "Friss\u00edtette a megl\u00e9v\u0151 konfigur\u00e1ci\u00f3t." + "existing_instance_updated": "A megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 friss\u00edtve." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index df9b6c03bc5..e389261748d 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, @@ -14,7 +17,7 @@ "port": "Porta", "ssl": "Utiliza um certificado SSL", "username": "Nome de Utilizador", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 12c5a4593c5..d50224698b8 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "async_change": async_change, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 73b10b158b0..c0934239fe7 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity PARALLEL_UPDATES = 0 @@ -21,13 +21,13 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up AdvantageAir motion platform.""" + """Set up AdvantageAir Binary Sensor platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] entities: list[BinarySensorEntity] = [] for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - entities.append(AdvantageAirZoneFilter(instance, ac_key)) + entities.append(AdvantageAirFilter(instance, ac_key)) for zone_key, zone in ac_device["zones"].items(): # Only add motion sensor when motion is enabled if zone["motionConfig"] >= 2: @@ -38,19 +38,17 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): - """Advantage Air Filter.""" +class AdvantageAirFilter(AdvantageAirAcEntity, BinarySensorEntity): + """Advantage Air Filter sensor.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_name = "Filter" def __init__(self, instance, ac_key): - """Initialize an Advantage Air Filter.""" + """Initialize an Advantage Air Filter sensor.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} Filter' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-filter' - ) + self._attr_unique_id += "-filter" @property def is_on(self): @@ -58,18 +56,16 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): return self._ac["filterCleanStatus"] -class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): - """Advantage Air Zone Motion.""" +class AdvantageAirZoneMotion(AdvantageAirZoneEntity, BinarySensorEntity): + """Advantage Air Zone Motion sensor.""" _attr_device_class = BinarySensorDeviceClass.MOTION def __init__(self, instance, ac_key, zone_key): - """Initialize an Advantage Air Zone Motion.""" + """Initialize an Advantage Air Zone Motion sensor.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} Motion' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-motion' - ) + self._attr_name = f'{self._zone["name"]} motion' + self._attr_unique_id += "-motion" @property def is_on(self): @@ -77,19 +73,17 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): return self._zone["motion"] == 20 -class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): - """Advantage Air Zone MyZone.""" +class AdvantageAirZoneMyZone(AdvantageAirZoneEntity, BinarySensorEntity): + """Advantage Air Zone MyZone sensor.""" _attr_entity_registry_enabled_default = False _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): - """Initialize an Advantage Air Zone MyZone.""" + """Initialize an Advantage Air Zone MyZone sensor.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} MyZone' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-myzone' - ) + self._attr_name = f'{self._zone["name"]} myZone' + self._attr_unique_id += "-myzone" @property def is_on(self): diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 192e1987902..e69dc06dd7d 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -15,7 +15,6 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -25,7 +24,7 @@ from .const import ( ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN, ) -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity ADVANTAGE_AIR_HVAC_MODES = { "heat": HVACMode.HEAT, @@ -53,7 +52,6 @@ ADVANTAGE_AIR_FAN_MODES = { HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()} FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100} -ADVANTAGE_AIR_SERVICE_SET_MYZONE = "set_myzone" ZONE_HVAC_MODES = [HVACMode.OFF, HVACMode.HEAT_COOL] PARALLEL_UPDATES = 0 @@ -79,26 +77,14 @@ async def async_setup_entry( entities.append(AdvantageAirZone(instance, ac_key, zone_key)) async_add_entities(entities) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - ADVANTAGE_AIR_SERVICE_SET_MYZONE, - {}, - "set_myzone", - ) - -class AdvantageAirClimateEntity(AdvantageAirEntity, ClimateEntity): - """AdvantageAir Climate class.""" +class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): + """AdvantageAir AC unit.""" _attr_temperature_unit = TEMP_CELSIUS _attr_target_temperature_step = PRECISION_WHOLE _attr_max_temp = 32 _attr_min_temp = 16 - - -class AdvantageAirAC(AdvantageAirClimateEntity): - """AdvantageAir AC unit.""" - _attr_fan_modes = [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] _attr_hvac_modes = AC_HVAC_MODES _attr_supported_features = ( @@ -108,8 +94,6 @@ class AdvantageAirAC(AdvantageAirClimateEntity): def __init__(self, instance, ac_key): """Initialize an AdvantageAir AC unit.""" super().__init__(instance, ac_key) - self._attr_name = self._ac["name"] - self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{ac_key}' if self._ac.get("myAutoModeEnabled"): self._attr_hvac_modes = AC_HVAC_MODES + [HVACMode.AUTO] @@ -160,9 +144,13 @@ class AdvantageAirAC(AdvantageAirClimateEntity): await self.async_change({self.ac_key: {"info": {"setTemp": temp}}}) -class AdvantageAirZone(AdvantageAirClimateEntity): +class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): """AdvantageAir Zone control.""" + _attr_temperature_unit = TEMP_CELSIUS + _attr_target_temperature_step = PRECISION_WHOLE + _attr_max_temp = 32 + _attr_min_temp = 16 _attr_hvac_modes = ZONE_HVAC_MODES _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE @@ -216,12 +204,3 @@ class AdvantageAirZone(AdvantageAirClimateEntity): await self.async_change( {self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}} ) - - async def set_myzone(self, **kwargs): - """Set this zone as the 'MyZone'.""" - _LOGGER.warning( - "The advantage_air.set_myzone service has been deprecated and will be removed in a future version, please use the select.select_option service on the MyZone entity" - ) - await self.async_change( - {self.ac_key: {"info": {"myZone": self._zone["number"]}}} - ) diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 36ae2c7fff0..4b3f371f52e 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -16,7 +16,7 @@ from .const import ( ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN, ) -from .entity import AdvantageAirEntity +from .entity import AdvantageAirZoneEntity PARALLEL_UPDATES = 0 @@ -39,8 +39,8 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): - """Advantage Air Cover Class.""" +class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): + """Advantage Air Zone Vent.""" _attr_device_class = CoverDeviceClass.DAMPER _attr_supported_features = ( @@ -50,12 +50,9 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): ) def __init__(self, instance, ac_key, zone_key): - """Initialize an Advantage Air Cover Class.""" + """Initialize an Advantage Air Zone Vent.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]}' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}' - ) + self._attr_name = self._zone["name"] @property def is_closed(self) -> bool: diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 9514cc7915b..375bfa255c4 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -9,24 +9,46 @@ from .const import DOMAIN class AdvantageAirEntity(CoordinatorEntity): """Parent class for Advantage Air Entities.""" - def __init__(self, instance, ac_key, zone_key=None): - """Initialize common aspects of an Advantage Air sensor.""" + _attr_has_entity_name = True + + def __init__(self, instance): + """Initialize common aspects of an Advantage Air entity.""" super().__init__(instance["coordinator"]) + self._attr_unique_id = self.coordinator.data["system"]["rid"] + + +class AdvantageAirAcEntity(AdvantageAirEntity): + """Parent class for Advantage Air AC Entities.""" + + def __init__(self, instance, ac_key): + """Initialize common aspects of an Advantage Air ac entity.""" + super().__init__(instance) self.async_change = instance["async_change"] self.ac_key = ac_key - self.zone_key = zone_key + self._attr_unique_id += f"-{ac_key}" + self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])}, + via_device=(DOMAIN, self.coordinator.data["system"]["rid"]), + identifiers={(DOMAIN, self._attr_unique_id)}, manufacturer="Advantage Air", model=self.coordinator.data["system"]["sysType"], - name=self.coordinator.data["system"]["name"], - sw_version=self.coordinator.data["system"]["myAppRev"], + name=self.coordinator.data["aircons"][self.ac_key]["info"]["name"], ) @property def _ac(self): return self.coordinator.data["aircons"][self.ac_key]["info"] + +class AdvantageAirZoneEntity(AdvantageAirAcEntity): + """Parent class for Advantage Air Zone Entities.""" + + def __init__(self, instance, ac_key, zone_key): + """Initialize common aspects of an Advantage Air zone entity.""" + super().__init__(instance, ac_key) + self.zone_key = zone_key + self._attr_unique_id += f"-{zone_key}" + @property def _zone(self): return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index ecc612ae1ed..9cfece25b24 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity ADVANTAGE_AIR_INACTIVE = "Inactive" @@ -15,7 +15,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up AdvantageAir toggle platform.""" + """Set up AdvantageAir select platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] @@ -25,21 +25,19 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): +class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): """Representation of Advantage Air MyZone control.""" _attr_icon = "mdi:home-thermometer" _attr_options = [ADVANTAGE_AIR_INACTIVE] _number_to_name = {0: ADVANTAGE_AIR_INACTIVE} _name_to_number = {ADVANTAGE_AIR_INACTIVE: 0} + _attr_name = "MyZone" def __init__(self, instance, ac_key): """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} MyZone' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-myzone' - ) + self._attr_unique_id += "-myzone" for zone in instance["coordinator"].data["aircons"][ac_key]["zones"].values(): if zone["type"] > 0: @@ -49,7 +47,7 @@ class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): @property def current_option(self): - """Return the fresh air status.""" + """Return the current MyZone.""" return self._number_to_name[self._ac["myZone"]] async def async_select_option(self, option): diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 8055c37a571..b110294b2fd 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity ADVANTAGE_AIR_SET_COUNTDOWN_VALUE = "minutes" ADVANTAGE_AIR_SET_COUNTDOWN_UNIT = "min" @@ -56,7 +56,7 @@ async def async_setup_entry( ) -class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): +class AdvantageAirTimeTo(AdvantageAirAcEntity, SensorEntity): """Representation of Advantage Air timer control.""" _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT @@ -67,10 +67,8 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): super().__init__(instance, ac_key) self.action = action self._time_key = f"countDownTo{action}" - self._attr_name = f'{self._ac["name"]} Time To {action}' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}' - ) + self._attr_name = f"Time to {action}" + self._attr_unique_id += f"-timeto{action}" @property def native_value(self): @@ -90,7 +88,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): await self.async_change({self.ac_key: {"info": {self._time_key: value}}}) -class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): +class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity): """Representation of Advantage Air Zone Vent Sensor.""" _attr_native_unit_of_measurement = PERCENTAGE @@ -100,10 +98,8 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Vent Sensor.""" super().__init__(instance, ac_key, zone_key=zone_key) - self._attr_name = f'{self._zone["name"]} Vent' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-vent' - ) + self._attr_name = f'{self._zone["name"]} vent' + self._attr_unique_id += "-vent" @property def native_value(self): @@ -120,7 +116,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): return "mdi:fan-off" -class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): +class AdvantageAirZoneSignal(AdvantageAirZoneEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" _attr_native_unit_of_measurement = PERCENTAGE @@ -130,10 +126,8 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone wireless signal sensor.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} Signal' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal' - ) + self._attr_name = f'{self._zone["name"]} signal' + self._attr_unique_id += "-signal" @property def native_value(self): @@ -154,7 +148,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): return "mdi:wifi-strength-outline" -class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): +class AdvantageAirZoneTemp(AdvantageAirZoneEntity, SensorEntity): """Representation of Advantage Air Zone temperature sensor.""" _attr_native_unit_of_measurement = TEMP_CELSIUS @@ -166,10 +160,8 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Temp Sensor.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} Temperature' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-temp' - ) + self._attr_name = f'{self._zone["name"]} temperature' + self._attr_unique_id += "-temp" @property def native_value(self): diff --git a/homeassistant/components/advantage_air/services.yaml b/homeassistant/components/advantage_air/services.yaml index 38f21e57128..6bd3bf815d6 100644 --- a/homeassistant/components/advantage_air/services.yaml +++ b/homeassistant/components/advantage_air/services.yaml @@ -15,11 +15,3 @@ set_time_to: min: 0 max: 1440 unit_of_measurement: minutes - -set_myzone: - name: Set MyZone - description: Change which zone is set as the reference for temperature control (deprecated) - target: - entity: - integration: advantage_air - domain: climate diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 44be859aa63..d9d46427599 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -9,7 +9,7 @@ from .const import ( ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN, ) -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity async def async_setup_entry( @@ -17,7 +17,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up AdvantageAir toggle platform.""" + """Set up AdvantageAir switch platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] @@ -28,18 +28,16 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirFreshAir(AdvantageAirEntity, SwitchEntity): +class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity): """Representation of Advantage Air fresh air control.""" _attr_icon = "mdi:air-filter" + _attr_name = "Fresh air" def __init__(self, instance, ac_key): """Initialize an Advantage Air fresh air control.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} Fresh Air' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-freshair' - ) + self._attr_unique_id += "-freshair" @property def is_on(self): diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index a914a23a0da..032e0a3a9f6 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json index fd79c699cee..1279f90beaa 100644 --- a/homeassistant/components/aemet/translations/ja.json +++ b/homeassistant/components/aemet/translations/ja.json @@ -12,9 +12,9 @@ "api_key": "API\u30ad\u30fc", "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", - "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" + "name": "\u7d71\u5408\u306e\u540d\u524d" }, - "description": "AEMET OpenData\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "AEMET OpenData\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/aemet/translations/pt.json b/homeassistant/components/aemet/translations/pt.json new file mode 100644 index 00000000000..cc227afe3a3 --- /dev/null +++ b/homeassistant/components/aemet/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index a2831fe301e..6723d62e9e0 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b sw_version=agent_client.version, ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 31396ecf51b..a3feab1e6f8 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove air_quality entities from registry if they exist ent_reg = er.async_get(hass) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 801bca58412..8fddaea8ec2 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -25,7 +25,6 @@ SUFFIX_LIMIT: Final = "LIMIT" ATTRIBUTION: Final = "Data provided by Airly" CONF_USE_NEAREST: Final = "use_nearest" -DEFAULT_NAME: Final = "Airly" DOMAIN: Final = "airly" LABEL_ADVICE: Final = "advice" MANUFACTURER: Final = "Airly sp. z o.o." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 9b647b93afa..eeb0037c814 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -45,7 +45,6 @@ from .const import ( ATTR_LIMIT, ATTR_PERCENT, ATTRIBUTION, - DEFAULT_NAME, DOMAIN, MANUFACTURER, SUFFIX_LIMIT, @@ -137,6 +136,7 @@ async def async_setup_entry( class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): """Define an Airly sensor.""" + _attr_has_entity_name = True entity_description: AirlySensorEntityDescription def __init__( @@ -151,12 +151,11 @@ class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")}, manufacturer=MANUFACTURER, - name=DEFAULT_NAME, + name=name, configuration_url=URL.format( latitude=coordinator.latitude, longitude=coordinator.longitude ), ) - self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{description.key}".lower() ) diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 6ff5e2216a6..fbc64fa69ea 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -15,7 +15,7 @@ "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, - "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index 72d35ec97a7..9ae6243d2fb 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera Airly", + "can_reach_server": "Dost\u0119p do serwera", "requests_per_day": "Dozwolone dzienne \u017c\u0105dania", "requests_remaining": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" } diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 9c6babe1136..7c26cded4de 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airnow/translations/ja.json b/homeassistant/components/airnow/translations/ja.json index 17a1b14e164..6cf1e763eda 100644 --- a/homeassistant/components/airnow/translations/ja.json +++ b/homeassistant/components/airnow/translations/ja.json @@ -17,7 +17,7 @@ "longitude": "\u7d4c\u5ea6", "radius": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u534a\u5f84(\u30de\u30a4\u30eb; \u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index 352c0249637..423e890a855 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airthings/translations/pt.json b/homeassistant/components/airthings/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/airthings/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/__init__.py b/homeassistant/components/airtouch4/__init__.py index 7b0673ecfe4..a2c3f716ab1 100644 --- a/homeassistant/components/airtouch4/__init__.py +++ b/homeassistant/components/airtouch4/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airtouch4/translations/pt.json b/homeassistant/components/airtouch4/translations/pt.json new file mode 100644 index 00000000000..4e8578a0a28 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Anfitri\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 986c5306e27..a2a3d76c3db 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -273,7 +273,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if CONF_API_KEY in entry.data: async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY]) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 52d046cef42..e28a11666da 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -62,20 +62,20 @@ SENSOR_KIND_VOC = "voc" GEOGRAPHY_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_LEVEL, - name="Air Pollution Level", + name="Air pollution level", device_class=DEVICE_CLASS_POLLUTANT_LEVEL, icon="mdi:gauge", ), SensorEntityDescription( key=SENSOR_KIND_AQI, - name="Air Quality Index", + name="Air quality index", device_class=SensorDeviceClass.AQI, native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_POLLUTANT, - name="Main Pollutant", + name="Main pollutant", device_class=DEVICE_CLASS_POLLUTANT_LABEL, icon="mdi:chemical-weapon", ), @@ -85,7 +85,7 @@ GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."} NODE_PRO_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_AQI, - name="Air Quality Index", + name="Air quality index", device_class=SensorDeviceClass.AQI, native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, @@ -292,6 +292,8 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to a Node/Pro unit.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, @@ -301,9 +303,6 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Initialize.""" super().__init__(coordinator, entry, description) - self._attr_name = ( - f"{coordinator.data['settings']['node_name']} Node/Pro: {description.name}" - ) self._attr_unique_id = f"{coordinator.data['serial_number']}_{description.key}" @property diff --git a/homeassistant/components/airvisual/translations/pt.json b/homeassistant/components/airvisual/translations/pt.json index cc1c500946d..75869241f0d 100644 --- a/homeassistant/components/airvisual/translations/pt.json +++ b/homeassistant/components/airvisual/translations/pt.json @@ -10,6 +10,11 @@ "invalid_api_key": "Chave de API inv\u00e1lida" }, "step": { + "geography_by_coords": { + "data": { + "latitude": "Latitude" + } + }, "node_pro": { "data": { "ip_address": "Servidor", @@ -18,7 +23,7 @@ }, "reauth_confirm": { "data": { - "api_key": "" + "api_key": "Chave da API" } } } diff --git a/homeassistant/components/airvisual/translations/sensor.pt.json b/homeassistant/components/airvisual/translations/sensor.pt.json new file mode 100644 index 00000000000..f7a976b51ed --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.pt.json @@ -0,0 +1,13 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Mon\u00f3xido de carbono", + "n2": "Di\u00f3xido de nitrog\u00e9nio", + "o3": "Ozono", + "p1": "PM10" + }, + "airvisual__pollutant_level": { + "moderate": "Moderado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index cf57a28a5e1..65ce9193e07 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index e189ae741ad..e4f94327708 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.5"], + "requirements": ["aioairzone==0.4.6"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/homeassistant/components/airzone/translations/ja.json b/homeassistant/components/airzone/translations/ja.json index 28ebf809dbf..68bee99ba29 100644 --- a/homeassistant/components/airzone/translations/ja.json +++ b/homeassistant/components/airzone/translations/ja.json @@ -13,7 +13,7 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" }, - "description": "Airzone\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "description": "Airzone\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/airzone/translations/pt.json b/homeassistant/components/airzone/translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/airzone/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 82b0c8ec692..40dd0dd981a 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -16,7 +16,7 @@ from .const import CLIENT_ID, DOMAIN _LOGGER: Final = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.COVER] +PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady("Can not connect to host") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 5c28d649936..f032fcecbe0 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -24,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -87,9 +88,19 @@ class AladdinDevice(CoverEntity): self._device_id = device["device_id"] self._number = device["door_number"] - self._attr_name = device["name"] + self._name = device["name"] self._serial = device["serial"] self._attr_unique_id = f"{self._device_id}-{self._number}" + self._attr_has_entity_name = True + + @property + def device_info(self) -> DeviceInfo | None: + """Device information for Aladdin Connect cover.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self._name, + manufacturer="Overhead Door", + ) async def async_added_to_hass(self) -> None: """Connect Aladdin Connect to the cloud.""" diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 4a04bf69aed..5e55f391aa6 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.27"], + "requirements": ["AIOAladdinConnect==0.1.39"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py new file mode 100644 index 00000000000..68631c57fc8 --- /dev/null +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -0,0 +1,116 @@ +"""Support for Aladdin Connect Garage Door sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast + +from AIOAladdinConnect import AladdinConnectClient + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .model import DoorDevice + + +@dataclass +class AccSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable + + +@dataclass +class AccSensorEntityDescription( + SensorEntityDescription, AccSensorEntityDescriptionMixin +): + """Describes AladdinConnect sensor entity.""" + + +SENSORS: tuple[AccSensorEntityDescription, ...] = ( + AccSensorEntityDescription( + key="battery_level", + name="Battery level", + device_class=SensorDeviceClass.BATTERY, + entity_registry_enabled_default=False, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=AladdinConnectClient.get_battery_status, + ), + AccSensorEntityDescription( + key="rssi", + name="Wi-Fi RSSI", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=AladdinConnectClient.get_rssi_status, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Aladdin Connect sensor devices.""" + + acc: AladdinConnectClient = hass.data[DOMAIN][entry.entry_id] + + entities = [] + doors = await acc.get_doors() + + for door in doors: + entities.extend( + [AladdinConnectSensor(acc, door, description) for description in SENSORS] + ) + + async_add_entities(entities) + + +class AladdinConnectSensor(SensorEntity): + """A sensor implementation for Aladdin Connect devices.""" + + _device: AladdinConnectSensor + entity_description: AccSensorEntityDescription + + def __init__( + self, + acc: AladdinConnectClient, + device: DoorDevice, + description: AccSensorEntityDescription, + ) -> None: + """Initialize a sensor for an Abode device.""" + self._device_id = device["device_id"] + self._number = device["door_number"] + self._name = device["name"] + self._acc = acc + self.entity_description = description + self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}" + self._attr_has_entity_name = True + + @property + def device_info(self) -> DeviceInfo | None: + """Device information for Aladdin Connect sensors.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self._name, + manufacturer="Overhead Door", + ) + + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + return cast( + float, + self.entity_description.value_fn(self._acc, self._device_id, self._number), + ) diff --git a/homeassistant/components/aladdin_connect/translations/ja.json b/homeassistant/components/aladdin_connect/translations/ja.json index 196b7f1d066..8859f24cd54 100644 --- a/homeassistant/components/aladdin_connect/translations/ja.json +++ b/homeassistant/components/aladdin_connect/translations/ja.json @@ -13,8 +13,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "Aladdin Connect\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Aladdin Connect\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/aladdin_connect/translations/pt.json b/homeassistant/components/aladdin_connect/translations/pt.json new file mode 100644 index 00000000000..6c09ed1c852 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/pt.json b/homeassistant/components/alarm_control_panel/translations/pt.json index e4293b81731..fb8cce10c93 100644 --- a/homeassistant/components/alarm_control_panel/translations/pt.json +++ b/homeassistant/components/alarm_control_panel/translations/pt.json @@ -13,6 +13,7 @@ "armed_custom_bypass": "Armado com desvio personalizado", "armed_home": "Armado Casa", "armed_night": "Armado noite", + "armed_vacation": "Armado f\u00e9rias", "arming": "A armar", "disarmed": "Desarmado", "disarming": "A desarmar", diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index d5c1b88e08e..7206b24632b 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -131,7 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await open_connection() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/alarmdecoder/translations/bg.json b/homeassistant/components/alarmdecoder/translations/bg.json index b918c0c7710..bf4f4b7175c 100644 --- a/homeassistant/components/alarmdecoder/translations/bg.json +++ b/homeassistant/components/alarmdecoder/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "protocol": { "data": { diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 25ec43b689c..3963372fec1 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -903,6 +903,7 @@ class AlexaContactSensor(AlexaCapability): "en-CA", "en-IN", "en-US", + "en-GB", "es-ES", "it-IT", "ja-JP", @@ -951,6 +952,7 @@ class AlexaMotionSensor(AlexaCapability): "en-CA", "en-IN", "en-US", + "en-GB", "es-ES", "it-IT", "ja-JP", diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index b6cbe6ba74b..9f51d92a229 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -65,6 +65,7 @@ class AbstractConfig(ABC): async def async_enable_proactive_mode(self): """Enable proactive mode.""" + _LOGGER.debug("Enable proactive mode") if self._unsub_proactive_report is None: self._unsub_proactive_report = self.hass.async_create_task( async_enable_proactive_mode(self.hass, self) @@ -77,6 +78,7 @@ class AbstractConfig(ABC): async def async_disable_proactive_mode(self): """Disable proactive mode.""" + _LOGGER.debug("Disable proactive mode") if unsub_func := await self._unsub_proactive_report: unsub_func() self._unsub_proactive_report = None @@ -113,7 +115,6 @@ class AbstractConfig(ABC): self._store.set_authorized(authorized) if self.should_report_state != self.is_reporting_states: if self.should_report_state: - _LOGGER.debug("Enable proactive mode") try: await self.async_enable_proactive_mode() except Exception: @@ -121,7 +122,6 @@ class AbstractConfig(ABC): self._store.set_authorized(False) raise else: - _LOGGER.debug("Disable proactive mode") await self.async_disable_proactive_mode() diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 6a953a9f9d4..9be7381adb6 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -12,7 +12,6 @@ from .auth import Auth from .config import AbstractConfig from .const import CONF_ENDPOINT, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE from .smart_home import async_handle_message -from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" @@ -104,7 +103,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: hass.http.register_view(SmartHomeView(smart_home_config)) if smart_home_config.should_report_state: - await async_enable_proactive_mode(hass, smart_home_config) + await smart_home_config.async_enable_proactive_mode() class SmartHomeView(HomeAssistantView): diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 15c280d9c1e..09ff85491ba 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -5,7 +5,7 @@ import asyncio from datetime import timedelta import logging import time -from typing import Optional, cast +from typing import Any from aiohttp import ClientError, ClientSession import async_timeout @@ -167,8 +167,8 @@ async def _configure_almond_for_ha( return _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url) - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) - data = cast(Optional[dict], await store.async_load()) + store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + data = await store.async_load() if data is None: data = {} diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json index 1d41aa41f87..898bc4bc1c0 100644 --- a/homeassistant/components/almond/translations/ja.json +++ b/homeassistant/components/almond/translations/ja.json @@ -4,7 +4,7 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py index 4481afb09ca..dbc503928c4 100644 --- a/homeassistant/components/ambee/__init__.py +++ b/homeassistant/components/ambee/__init__.py @@ -3,10 +3,16 @@ from __future__ import annotations from ambee import AirQuality, Ambee, AmbeeAuthenticationError, Pollen +from homeassistant.components.repairs import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN @@ -14,6 +20,20 @@ from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_P PLATFORMS = [Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Ambee integration.""" + async_create_issue( + hass, + DOMAIN, + "pending_removal", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ambee from a config entry.""" hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {}) @@ -58,7 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await pollen.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id][SERVICE_POLLEN] = pollen - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -67,4 +87,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + async_delete_issue(hass, DOMAIN, "pending_removal") return unload_ok diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index 8f8f2237654..83abb841629 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -27,7 +27,7 @@ SERVICE_AIR_QUALITY: Final = "air_quality" SERVICE_POLLEN: Final = "pollen" SERVICES: dict[str, str] = { - SERVICE_AIR_QUALITY: "Air Quality", + SERVICE_AIR_QUALITY: "Air quality", SERVICE_POLLEN: "Pollen", } @@ -35,25 +35,25 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { SERVICE_AIR_QUALITY: [ SensorEntityDescription( key="particulate_matter_2_5", - name="Particulate Matter < 2.5 μm", + name="Particulate matter < 2.5 μm", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="particulate_matter_10", - name="Particulate Matter < 10 μm", + name="Particulate matter < 10 μm", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="sulphur_dioxide", - name="Sulphur Dioxide (SO2)", + name="Sulphur dioxide (SO2)", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="nitrogen_dioxide", - name="Nitrogen Dioxide (NO2)", + name="Nitrogen dioxide (NO2)", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=SensorStateClass.MEASUREMENT, ), @@ -65,60 +65,60 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="carbon_monoxide", - name="Carbon Monoxide (CO)", + name="Carbon monoxide (CO)", device_class=SensorDeviceClass.CO, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="air_quality_index", - name="Air Quality Index (AQI)", + name="Air quality index (AQI)", state_class=SensorStateClass.MEASUREMENT, ), ], SERVICE_POLLEN: [ SensorEntityDescription( key="grass", - name="Grass Pollen", + name="Grass", icon="mdi:grass", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="tree", - name="Tree Pollen", + name="Tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="weed", - name="Weed Pollen", + name="Weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="grass_risk", - name="Grass Pollen Risk", + name="Grass risk", icon="mdi:grass", device_class=DEVICE_CLASS_AMBEE_RISK, ), SensorEntityDescription( key="tree_risk", - name="Tree Pollen Risk", + name="Tree risk", icon="mdi:tree", device_class=DEVICE_CLASS_AMBEE_RISK, ), SensorEntityDescription( key="weed_risk", - name="Weed Pollen Risk", + name="Weed risk", icon="mdi:sprout", device_class=DEVICE_CLASS_AMBEE_RISK, ), SensorEntityDescription( key="grass_poaceae", - name="Poaceae Grass Pollen", + name="Poaceae grass", icon="mdi:grass", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -126,7 +126,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_alder", - name="Alder Tree Pollen", + name="Alder tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -134,7 +134,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_birch", - name="Birch Tree Pollen", + name="Birch tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -142,7 +142,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_cypress", - name="Cypress Tree Pollen", + name="Cypress tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -150,7 +150,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_elm", - name="Elm Tree Pollen", + name="Elm tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -158,7 +158,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_hazel", - name="Hazel Tree Pollen", + name="Hazel tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -166,7 +166,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_oak", - name="Oak Tree Pollen", + name="Oak tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -174,7 +174,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_pine", - name="Pine Tree Pollen", + name="Pine tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -182,7 +182,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_plane", - name="Plane Tree Pollen", + name="Plane tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -190,7 +190,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_poplar", - name="Poplar Tree Pollen", + name="Poplar tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -198,7 +198,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_chenopod", - name="Chenopod Weed Pollen", + name="Chenopod weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -206,7 +206,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_mugwort", - name="Mugwort Weed Pollen", + name="Mugwort weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -214,7 +214,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_nettle", - name="Nettle Weed Pollen", + name="Nettle weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -222,7 +222,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_ragweed", - name="Ragweed Weed Pollen", + name="Ragweed weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json index 3226e9de3a3..f74832100cd 100644 --- a/homeassistant/components/ambee/manifest.json +++ b/homeassistant/components/ambee/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambee", "requirements": ["ambee==0.4.0"], + "dependencies": ["repairs"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index bf9cfe74f31..8fb6c9f2a61 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -42,6 +42,8 @@ async def async_setup_entry( class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): """Defines an Ambee sensor.""" + _attr_has_entity_name = True + def __init__( self, *, diff --git a/homeassistant/components/ambee/strings.json b/homeassistant/components/ambee/strings.json index e3c306788dd..7d0e75877c9 100644 --- a/homeassistant/components/ambee/strings.json +++ b/homeassistant/components/ambee/strings.json @@ -24,5 +24,11 @@ "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "issues": { + "pending_removal": { + "title": "The Ambee integration is being removed", + "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue." + } } } diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json index 4359ab72349..8055ef5210f 100644 --- a/homeassistant/components/ambee/translations/de.json +++ b/homeassistant/components/ambee/translations/de.json @@ -24,5 +24,11 @@ "description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein." } } + }, + "issues": { + "pending_removal": { + "description": "Die Ambee-Integration ist dabei, aus Home Assistant entfernt zu werden und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil Ambee seine kostenlosen (begrenzten) Konten entfernt hat und keine M\u00f6glichkeit mehr f\u00fcr regul\u00e4re Nutzer bietet, sich f\u00fcr einen kostenpflichtigen Plan anzumelden.\n\nEntferne den Ambee-Integrationseintrag aus deiner Instanz, um dieses Problem zu beheben.", + "title": "Die Ambee-Integration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/en.json b/homeassistant/components/ambee/translations/en.json index 433580e8023..03f4c3241b6 100644 --- a/homeassistant/components/ambee/translations/en.json +++ b/homeassistant/components/ambee/translations/en.json @@ -24,5 +24,11 @@ "description": "Set up Ambee to integrate with Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue.", + "title": "The Ambee integration is being removed" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json index fe97ce33686..db330a9b239 100644 --- a/homeassistant/components/ambee/translations/it.json +++ b/homeassistant/components/ambee/translations/it.json @@ -24,5 +24,11 @@ "description": "Configura Ambee per l'integrazione con Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.", + "title": "L'integrazione Ambee verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pl.json b/homeassistant/components/ambee/translations/pl.json index d0b2225cc9a..255d402175d 100644 --- a/homeassistant/components/ambee/translations/pl.json +++ b/homeassistant/components/ambee/translations/pl.json @@ -24,5 +24,11 @@ "description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem." } } + }, + "issues": { + "pending_removal": { + "description": "Integracja Ambee oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c Ambee usun\u0105\u0142 ich bezp\u0142atne (ograniczone) konta i nie zapewnia ju\u017c zwyk\u0142ym u\u017cytkownikom mo\u017cliwo\u015bci zarejestrowania si\u0119 w p\u0142atnym planie. \n\nUsu\u0144 integracj\u0119 Ambee z Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Ambee zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt-BR.json b/homeassistant/components/ambee/translations/pt-BR.json index 2d960e17df2..3220de5104e 100644 --- a/homeassistant/components/ambee/translations/pt-BR.json +++ b/homeassistant/components/ambee/translations/pt-BR.json @@ -24,5 +24,11 @@ "description": "Configure o Ambee para integrar com o Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do Ambee est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, porque a Ambee removeu suas contas gratuitas (limitadas) e n\u00e3o oferece mais uma maneira de usu\u00e1rios regulares se inscreverem em um plano pago. \n\n Remova a entrada de integra\u00e7\u00e3o Ambee de sua inst\u00e2ncia para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o Ambee est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt.json b/homeassistant/components/ambee/translations/pt.json new file mode 100644 index 00000000000..4a6d267473b --- /dev/null +++ b/homeassistant/components/ambee/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "latitude": "Latitude", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json index 2e1de25fde2..ccebea49c6f 100644 --- a/homeassistant/components/ambee/translations/zh-Hant.json +++ b/homeassistant/components/ambee/translations/zh-Hant.json @@ -24,5 +24,11 @@ "description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" } } + }, + "issues": { + "pending_removal": { + "description": "Ambee \u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc Ambee \u79fb\u9664\u4e86\u5176\u514d\u8cbb\uff08\u6709\u9650\uff09\u5e33\u865f\u3001\u4e26\u4e14\u4e0d\u518d\u63d0\u4f9b\u4e00\u822c\u4f7f\u7528\u8005\u8a3b\u518a\u4ed8\u8cbb\u670d\u52d9\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Ambee \u6574\u5408\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/amberelectric/__init__.py b/homeassistant/components/amberelectric/__init__.py index 0d39077f2f1..b6901e1b81b 100644 --- a/homeassistant/components/amberelectric/__init__.py +++ b/homeassistant/components/amberelectric/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = AmberUpdateCoordinator(hass, api_instance, site_id) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index acab01e30d1..240c9780cee 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -41,5 +41,5 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ambiclimate from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 93d9348655f..50135693ff4 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -64,7 +64,7 @@ async def async_setup_entry( """Set up the Ambiclimate device from config entry.""" config = entry.data websession = async_get_clientsession(hass) - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) token_info = await store.async_load() oauth = ambiclimate.AmbiclimateOAuth( diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 3de93421f42..92ea617393e 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "K\u00e9rj\u00fck, k\u00f6vesse ezt a [link]({authorization_url}}) \u00e9s **Enged\u00e9lyezze** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", + "description": "K\u00e9rem, k\u00f6vesse ezt a [link]({authorization_url}}) \u00e9s **Enged\u00e9lyezze** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", "title": "Ambiclimate hiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index fb4a865a72e..7242c0ba53b 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -198,6 +198,7 @@ class AmbientStation: class AmbientWeatherEntity(Entity): """Define a base Ambient PWS entity.""" + _attr_has_entity_name = True _attr_should_poll = False def __init__( @@ -215,10 +216,9 @@ class AmbientWeatherEntity(Entity): configuration_url=f"https://ambientweather.net/dashboard/{public_device_id}", identifiers={(DOMAIN, mac_address)}, manufacturer="Ambient Weather", - name=station_name, + name=station_name.capitalize(), ) - self._attr_name = f"{station_name}_{description.name}" self._attr_unique_id = f"{mac_address}_{description.key}" self._mac_address = mac_address self.entity_description = description diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 5fecbce3ded..4380e1839f2 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -143,112 +143,112 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), AmbientBinarySensorDescription( key=TYPE_BATTIN, - name="Interior Battery", + name="Interior battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT10, - name="Soil Monitor Battery 10", + name="Battery 10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM1, - name="Soil Monitor Battery 1", + name="Soil monitor battery 1", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM2, - name="Soil Monitor Battery 2", + name="Soil monitor battery 2", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM3, - name="Soil Monitor Battery 3", + name="Soil monitor battery 3", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM4, - name="Soil Monitor Battery 4", + name="Soil monitor battery 4", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM5, - name="Soil Monitor Battery 5", + name="Soil monitor battery 5", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM6, - name="Soil Monitor Battery 6", + name="Soil monitor battery 6", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM7, - name="Soil Monitor Battery 7", + name="Soil monitor battery 7", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM8, - name="Soil Monitor Battery 8", + name="Soil monitor battery 8", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM9, - name="Soil Monitor Battery 9", + name="Soil monitor battery 9", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM10, - name="Soil Monitor Battery 10", + name="Soil monitor battery 10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_CO2, - name="CO2 Battery", + name="CO2 battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_LIGHTNING, - name="Lightning Detector Battery", + name="Lightning detector battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25IN_BATT, - name="PM25 Indoor Battery", + name="PM25 indoor battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25_BATT, - name="PM25 Battery", + name="PM25 battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index a103316f3ff..4eae53b6a03 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -113,7 +113,7 @@ TYPE_YEARLYRAININ = "yearlyrainin" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_24HOURRAININ, - name="24 Hr Rain", + name="24 hr rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, @@ -126,74 +126,74 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_AQI_PM25_24H, - name="AQI PM2.5 24h Avg", + name="AQI PM2.5 24h avg", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN, - name="AQI PM2.5 Indoor", + name="AQI PM2.5 indoor", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN_24H, - name="AQI PM2.5 Indoor 24h Avg", + name="AQI PM2.5 indoor 24h avg", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_BAROMABSIN, - name="Abs Pressure", + name="Abs pressure", native_unit_of_measurement=PRESSURE_INHG, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_BAROMRELIN, - name="Rel Pressure", + name="Rel pressure", native_unit_of_measurement=PRESSURE_INHG, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CO2, - name="co2", + name="CO2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_DAILYRAININ, - name="Daily Rain", + name="Daily rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_DEWPOINT, - name="Dew Point", + name="Dew point", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_EVENTRAININ, - name="Event Rain", + name="Event rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_FEELSLIKE, - name="Feels Like", + name="Feels like", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HOURLYRAININ, - name="Hourly Rain Rate", + name="Hourly rain rate", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, @@ -266,60 +266,60 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_HUMIDITYIN, - name="Humidity In", + name="Humidity in", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_LASTRAIN, - name="Last Rain", + name="Last rain", icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_DAY, - name="Lightning Strikes Per Day", + name="Lightning strikes per day", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_HOUR, - name="Lightning Strikes Per Hour", + name="Lightning strikes per hour", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, - name="Max Gust", + name="Max gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_MONTHLYRAININ, - name="Monthly Rain", + name="Monthly rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_PM25_24H, - name="PM25 24h Avg", + name="PM25 24h avg", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, ), SensorEntityDescription( key=TYPE_PM25_IN, - name="PM25 Indoor", + name="PM25 indoor", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_PM25_IN_24H, - name="PM25 Indoor 24h Avg", + name="PM25 indoor 24h avg", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, ), @@ -332,144 +332,144 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_SOILHUM10, - name="Soil Humidity 10", + name="Soil humidity 10", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM1, - name="Soil Humidity 1", + name="Soil humidity 1", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM2, - name="Soil Humidity 2", + name="Soil humidity 2", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM3, - name="Soil Humidity 3", + name="Soil humidity 3", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM4, - name="Soil Humidity 4", + name="Soil humidity 4", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM5, - name="Soil Humidity 5", + name="Soil humidity 5", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM6, - name="Soil Humidity 6", + name="Soil humidity 6", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM7, - name="Soil Humidity 7", + name="Soil humidity 7", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM8, - name="Soil Humidity 8", + name="Soil humidity 8", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM9, - name="Soil Humidity 9", + name="Soil humidity 9", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILTEMP10F, - name="Soil Temp 10", + name="Soil temp 10", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP1F, - name="Soil Temp 1", + name="Soil temp 1", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP2F, - name="Soil Temp 2", + name="Soil temp 2", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP3F, - name="Soil Temp 3", + name="Soil temp 3", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP4F, - name="Soil Temp 4", + name="Soil temp 4", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP5F, - name="Soil Temp 5", + name="Soil temp 5", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP6F, - name="Soil Temp 6", + name="Soil temp 6", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP7F, - name="Soil Temp 7", + name="Soil temp 7", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP8F, - name="Soil Temp 8", + name="Soil temp 8", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP9F, - name="Soil Temp 9", + name="Soil temp 9", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION, - name="Solar Rad", + name="Solar rad", native_unit_of_measurement=IRRADIATION_WATTS_PER_SQUARE_METER, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION_LX, - name="Solar Rad", + name="Solar rad", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, @@ -553,85 +553,85 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_TEMPINF, - name="Inside Temp", + name="Inside temp", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TOTALRAININ, - name="Lifetime Rain", + name="Lifetime rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_UV, - name="UV Index", + name="UV index", native_unit_of_measurement="Index", device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WEEKLYRAININ, - name="Weekly Rain", + name="Weekly rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WINDDIR, - name="Wind Dir", + name="Wind dir", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDDIR_AVG10M, - name="Wind Dir Avg 10m", + name="Wind dir avg 10m", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDDIR_AVG2M, - name="Wind Dir Avg 2m", + name="Wind dir avg 2m", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDGUSTDIR, - name="Gust Dir", + name="Gust dir", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDGUSTMPH, - name="Wind Gust", + name="Wind gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG10M, - name="Wind Avg 10m", + name="Wind avg 10m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG2M, - name="Wind Avg 2m", + name="Wind avg 2m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, ), SensorEntityDescription( key=TYPE_WINDSPEEDMPH, - name="Wind Speed", + name="Wind speed", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_YEARLYRAININ, - name="Yearly Rain", + name="Yearly rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, diff --git a/homeassistant/components/ambient_station/translations/pt.json b/homeassistant/components/ambient_station/translations/pt.json index c67faa25f0b..f40c2b211a9 100644 --- a/homeassistant/components/ambient_station/translations/pt.json +++ b/homeassistant/components/ambient_station/translations/pt.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Chave de API", + "api_key": "Chave da API", "app_key": "Chave de aplica\u00e7\u00e3o" }, "title": "Preencha as suas informa\u00e7\u00f5es" diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 802aa33585a..1a696b0c206 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -1,6 +1,6 @@ """Analytics helper class for the analytics integration.""" import asyncio -from typing import cast +from typing import Any import uuid import aiohttp @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.storage import Store from homeassistant.helpers.system_info import async_get_system_info -from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.loader import IntegrationNotFound, async_get_integrations from homeassistant.setup import async_get_loaded_integrations from .const import ( @@ -66,12 +66,12 @@ class Analytics: """Initialize the Analytics class.""" self.hass: HomeAssistant = hass self.session = async_get_clientsession(hass) - self._data: dict = { + self._data: dict[str, Any] = { ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None, } - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) @property def preferences(self) -> dict: @@ -109,7 +109,7 @@ class Analytics: async def load(self) -> None: """Load preferences.""" - stored = cast(dict, await self._store.async_load()) + stored = await self._store.async_load() if stored: self._data = stored @@ -182,15 +182,9 @@ class Analytics: if self.preferences.get(ATTR_USAGE, False) or self.preferences.get( ATTR_STATISTICS, False ): - configured_integrations = await asyncio.gather( - *( - async_get_integration(self.hass, domain) - for domain in async_get_loaded_integrations(self.hass) - ), - return_exceptions=True, - ) - - for integration in configured_integrations: + domains = async_get_loaded_integrations(self.hass) + configured_integrations = await async_get_integrations(self.hass, domains) + for integration in configured_integrations.values(): if isinstance(integration, IntegrationNotFound): continue diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 4b203fc3757..6942ff7ffd0 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -149,7 +149,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ANDROID_DEV_OPT: entry.options.copy(), } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 7bddd13c833..92d4f806b39 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.4.2", + "adb-shell[async]==0.4.3", "androidtv[async]==0.0.67", "pure-python-adb[async]==0.3.0.dev0" ], diff --git a/homeassistant/components/androidtv/translations/bg.json b/homeassistant/components/androidtv/translations/bg.json index f912f17d257..5407acc2f55 100644 --- a/homeassistant/components/androidtv/translations/bg.json +++ b/homeassistant/components/androidtv/translations/bg.json @@ -16,5 +16,14 @@ } } } + }, + "options": { + "step": { + "apps": { + "data": { + "app_name": "\u0418\u043c\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/pt.json b/homeassistant/components/androidtv/translations/pt.json new file mode 100644 index 00000000000..09a78c773cc --- /dev/null +++ b/homeassistant/components/androidtv/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index 56b06e865c2..5ad845b52d6 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -1 +1,59 @@ -"""The anthemav component.""" +"""The Anthem A/V Receivers integration.""" +from __future__ import annotations + +import logging + +import anthemav + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ANTHEMAV_UDATE_SIGNAL, DOMAIN + +PLATFORMS = [Platform.MEDIA_PLAYER] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Anthem A/V Receivers from a config entry.""" + + @callback + def async_anthemav_update_callback(message): + """Receive notification from transport that new data exists.""" + _LOGGER.debug("Received update callback from AVR: %s", message) + async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.data[CONF_NAME]}") + + try: + avr = await anthemav.Connection.create( + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + update_callback=async_anthemav_update_callback, + ) + + except OSError as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + avr = hass.data[DOMAIN][entry.entry_id] + + if avr is not None: + _LOGGER.debug("Close avr connection") + avr.close() + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/anthemav/config_flow.py b/homeassistant/components/anthemav/config_flow.py new file mode 100644 index 00000000000..55e1fd42e07 --- /dev/null +++ b/homeassistant/components/anthemav/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow for Anthem A/V Receivers integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import anthemav +from anthemav.connection import Connection +from anthemav.device_error import DeviceError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac + +from .const import CONF_MODEL, DEFAULT_NAME, DEFAULT_PORT, DOMAIN + +DEVICE_TIMEOUT_SECONDS = 4.0 + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + + +async def connect_device(user_input: dict[str, Any]) -> Connection: + """Connect to the AVR device.""" + avr = await anthemav.Connection.create( + host=user_input[CONF_HOST], port=user_input[CONF_PORT], auto_reconnect=False + ) + await avr.reconnect() + await avr.protocol.wait_for_device_initialised(DEVICE_TIMEOUT_SECONDS) + return avr + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Anthem A/V Receivers.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + if CONF_NAME not in user_input: + user_input[CONF_NAME] = DEFAULT_NAME + + errors = {} + + avr: Connection | None = None + try: + avr = await connect_device(user_input) + except OSError: + _LOGGER.error( + "Couldn't establish connection to %s:%s", + user_input[CONF_HOST], + user_input[CONF_PORT], + ) + errors["base"] = "cannot_connect" + except DeviceError: + _LOGGER.error( + "Couldn't receive device information from %s:%s", + user_input[CONF_HOST], + user_input[CONF_PORT], + ) + errors["base"] = "cannot_receive_deviceinfo" + else: + user_input[CONF_MAC] = format_mac(avr.protocol.macaddress) + user_input[CONF_MODEL] = avr.protocol.model + await self.async_set_unique_id(user_input[CONF_MAC]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + finally: + if avr is not None: + avr.close() + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/anthemav/const.py b/homeassistant/components/anthemav/const.py new file mode 100644 index 00000000000..c63a477f7f9 --- /dev/null +++ b/homeassistant/components/anthemav/const.py @@ -0,0 +1,7 @@ +"""Constants for the Anthem A/V Receivers integration.""" +ANTHEMAV_UDATE_SIGNAL = "anthemav_update" +CONF_MODEL = "model" +DEFAULT_NAME = "Anthem AV" +DEFAULT_PORT = 14999 +DOMAIN = "anthemav" +MANUFACTURER = "Anthem" diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index c43b976416d..27db9df32a3 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -2,8 +2,10 @@ "domain": "anthemav", "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", - "requirements": ["anthemav==1.2.0"], - "codeowners": [], + "requirements": ["anthemav==1.3.2"], + "dependencies": ["repairs"], + "codeowners": ["@hyralex"], + "config_flow": true, "iot_class": "local_push", "loggers": ["anthemav"] } diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 05bfea7ef45..3c3a363a6db 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -2,8 +2,9 @@ from __future__ import annotations import logging +from typing import Any -import anthemav +from anthemav.connection import Connection import voluptuous as vol from homeassistant.components.media_player import ( @@ -11,33 +12,38 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, + CONF_MAC, CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import ( + ANTHEMAV_UDATE_SIGNAL, + CONF_MODEL, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, + MANUFACTURER, +) + _LOGGER = logging.getLogger(__name__) -DOMAIN = "anthemav" - -DEFAULT_PORT = 14999 - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, } ) @@ -50,30 +56,45 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up our socket to the AVR.""" - - host = config[CONF_HOST] - port = config[CONF_PORT] - name = config.get(CONF_NAME) - device = None - - _LOGGER.info("Provisioning Anthem AVR device at %s:%d", host, port) - - @callback - def async_anthemav_update_callback(message): - """Receive notification from transport that new data exists.""" - _LOGGER.debug("Received update callback from AVR: %s", message) - async_dispatcher_send(hass, DOMAIN) - - avr = await anthemav.Connection.create( - host=host, port=port, update_callback=async_anthemav_update_callback + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + _LOGGER.warning( + "Configuration of the Anthem A/V Receivers integration in YAML is " + "deprecated and will be removed in Home Assistant 2022.10; Your " + "existing configuration has been imported into the UI automatically " + "and can be safely removed from your configuration.yaml file" + ) + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - device = AnthemAVR(avr, name) + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entry.""" + name = config_entry.data[CONF_NAME] + macaddress = config_entry.data[CONF_MAC] + model = config_entry.data[CONF_MODEL] + + avr = hass.data[DOMAIN][config_entry.entry_id] + + device = AnthemAVR(avr, name, macaddress, model) _LOGGER.debug("dump_devicedata: %s", device.dump_avrdata) _LOGGER.debug("dump_conndata: %s", avr.dump_conndata) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.avr.close) async_add_entities([device]) @@ -89,23 +110,34 @@ class AnthemAVR(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, avr, name): + def __init__(self, avr: Connection, name: str, macaddress: str, model: str) -> None: """Initialize entity with transport.""" super().__init__() self.avr = avr - self._attr_name = name or self._lookup("model") + self._attr_name = name + self._attr_unique_id = macaddress + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, macaddress)}, + name=name, + manufacturer=MANUFACTURER, + model=model, + ) - def _lookup(self, propname, dval=None): + def _lookup(self, propname: str, dval: Any | None = None) -> Any | None: return getattr(self.avr.protocol, propname, dval) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self.async_on_remove( - async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) + async_dispatcher_connect( + self.hass, + f"{ANTHEMAV_UDATE_SIGNAL}_{self._attr_name}", + self.async_write_ha_state, + ) ) @property - def state(self): + def state(self) -> str | None: """Return state of power on/off.""" pwrstate = self._lookup("power") @@ -116,22 +148,22 @@ class AnthemAVR(MediaPlayerEntity): return None @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool | None: """Return boolean reflecting mute state on device.""" return self._lookup("mute", False) @property - def volume_level(self): + def volume_level(self) -> float | None: """Return volume level from 0 to 1.""" return self._lookup("volume_as_percentage", 0.0) @property - def media_title(self): + def media_title(self) -> str | None: """Return current input name (closest we have to media title).""" return self._lookup("input_name", "No Source") @property - def app_name(self): + def app_name(self) -> str | None: """Return details about current video and audio stream.""" return ( f"{self._lookup('video_input_resolution_text', '')} " @@ -139,38 +171,38 @@ class AnthemAVR(MediaPlayerEntity): ) @property - def source(self): + def source(self) -> str | None: """Return currently selected input.""" return self._lookup("input_name", "Unknown") @property - def source_list(self): + def source_list(self) -> list[str] | None: """Return all active, configured inputs.""" return self._lookup("input_list", ["Unknown"]) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Change AVR to the designated source (by name).""" self._update_avr("input_name", source) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn AVR power off.""" self._update_avr("power", False) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn AVR power on.""" self._update_avr("power", True) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set AVR volume (0 to 1).""" self._update_avr("volume_as_percentage", volume) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Engage AVR mute.""" self._update_avr("mute", mute) - def _update_avr(self, propname, value): + def _update_avr(self, propname: str, value: Any | None) -> None: """Update a property in the AVR.""" - _LOGGER.info("Sending command to AVR: set %s to %s", propname, str(value)) + _LOGGER.debug("Sending command to AVR: set %s to %s", propname, str(value)) setattr(self.avr.protocol, propname, value) @property diff --git a/homeassistant/components/anthemav/strings.json b/homeassistant/components/anthemav/strings.json new file mode 100644 index 00000000000..b4e777c4de1 --- /dev/null +++ b/homeassistant/components/anthemav/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_receive_deviceinfo": "Failed to retreive MAC Address. Make sure the device is turned on" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Anthem A/V Receivers YAML configuration is being removed", + "description": "Configuring Anthem A/V Receivers using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Anthem A/V Receivers YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/anthemav/translations/ar.json b/homeassistant/components/anthemav/translations/ar.json new file mode 100644 index 00000000000..558784a02e4 --- /dev/null +++ b/homeassistant/components/anthemav/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0647\u0627\u0632 \u062a\u0645 \u062a\u0647\u064a\u0623\u062a\u0647 \u0645\u0633\u0628\u0642\u0627" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "host": "\u0645\u0636\u064a\u0641", + "port": "\u0645\u0646\u0641\u0630" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ca.json b/homeassistant/components/anthemav/translations/ca.json new file mode 100644 index 00000000000..723883d5c1a --- /dev/null +++ b/homeassistant/components/anthemav/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "cannot_receive_deviceinfo": "No s'ha pogut obtenir l'adre\u00e7a MAC. Assegura't que el dispositiu estigui enc\u00e8s" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/de.json b/homeassistant/components/anthemav/translations/de.json new file mode 100644 index 00000000000..d751349b005 --- /dev/null +++ b/homeassistant/components/anthemav/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "cannot_receive_deviceinfo": "MAC-Adresse konnte nicht abgerufen werden. Stelle sicher, dass das Ger\u00e4t eingeschaltet ist." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Anthem A/V-Receivern mit YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Anthem A/V Receivers YAML Konfiguration aus deiner configuration.yaml Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die YAML-Konfiguration von Anthem A/V Receivers wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/el.json b/homeassistant/components/anthemav/translations/el.json new file mode 100644 index 00000000000..983e89155e8 --- /dev/null +++ b/homeassistant/components/anthemav/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "cannot_receive_deviceinfo": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 MAC. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/en.json b/homeassistant/components/anthemav/translations/en.json new file mode 100644 index 00000000000..af4c83eb2a8 --- /dev/null +++ b/homeassistant/components/anthemav/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "cannot_receive_deviceinfo": "Failed to retreive MAC Address. Make sure the device is turned on" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Anthem A/V Receivers using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Anthem A/V Receivers YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Anthem A/V Receivers YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/et.json b/homeassistant/components/anthemav/translations/et.json new file mode 100644 index 00000000000..4ec356c8902 --- /dev/null +++ b/homeassistant/components/anthemav/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "cannot_receive_deviceinfo": "MAC-aadressi toomine eba\u00f5nnestus. Veendu, et seade oleks sisse l\u00fclitatud" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/fr.json b/homeassistant/components/anthemav/translations/fr.json new file mode 100644 index 00000000000..faf417552ce --- /dev/null +++ b/homeassistant/components/anthemav/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "cannot_receive_deviceinfo": "\u00c9chec de r\u00e9cup\u00e9ration de l'adresse MAC. Assurez-vous que l'appareil est allum\u00e9" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/hu.json b/homeassistant/components/anthemav/translations/hu.json new file mode 100644 index 00000000000..f13544fff61 --- /dev/null +++ b/homeassistant/components/anthemav/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "cannot_receive_deviceinfo": "Nem siker\u00fclt lek\u00e9rni a MAC-c\u00edmet. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a k\u00e9sz\u00fcl\u00e9k be van kapcsolva" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/id.json b/homeassistant/components/anthemav/translations/id.json new file mode 100644 index 00000000000..1eb2ba0b5a1 --- /dev/null +++ b/homeassistant/components/anthemav/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "cannot_receive_deviceinfo": "Gagal mengambil alamat MAC. Pastikan perangkat nyala" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json new file mode 100644 index 00000000000..b8bec832581 --- /dev/null +++ b/homeassistant/components/anthemav/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "cannot_receive_deviceinfo": "Impossibile recuperare l'indirizzo MAC. Assicurati che il dispositivo sia acceso" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Anthem A/V Receivers tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Anthem A/V Receivers verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ja.json b/homeassistant/components/anthemav/translations/ja.json new file mode 100644 index 00000000000..8c87d02e557 --- /dev/null +++ b/homeassistant/components/anthemav/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "cannot_receive_deviceinfo": "MAC\u30a2\u30c9\u30ec\u30b9\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/nl.json b/homeassistant/components/anthemav/translations/nl.json new file mode 100644 index 00000000000..c09dde1bfc3 --- /dev/null +++ b/homeassistant/components/anthemav/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/no.json b/homeassistant/components/anthemav/translations/no.json new file mode 100644 index 00000000000..e7b3f66ae8d --- /dev/null +++ b/homeassistant/components/anthemav/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "cannot_receive_deviceinfo": "Kunne ikke hente MAC-adressen. S\u00f8rg for at enheten er sl\u00e5tt p\u00e5" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pl.json b/homeassistant/components/anthemav/translations/pl.json new file mode 100644 index 00000000000..18eaecb9845 --- /dev/null +++ b/homeassistant/components/anthemav/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "cannot_receive_deviceinfo": "Nie uda\u0142o si\u0119 pobra\u0107 adresu MAC. Upewnij si\u0119, \u017ce urz\u0105dzenie jest w\u0142\u0105czone." + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json new file mode 100644 index 00000000000..5a6038bb480 --- /dev/null +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "cannot_receive_deviceinfo": "Falha ao recuperar o endere\u00e7o MAC. Verifique se o dispositivo est\u00e1 ligado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o de receptores A/V Anthem usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML dos receptores A/V do Anthem do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML dos receptores A/V do Anthem est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt.json b/homeassistant/components/anthemav/translations/pt.json new file mode 100644 index 00000000000..fa5aa3de317 --- /dev/null +++ b/homeassistant/components/anthemav/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ru.json b/homeassistant/components/anthemav/translations/ru.json new file mode 100644 index 00000000000..0f343609e4c --- /dev/null +++ b/homeassistant/components/anthemav/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "cannot_receive_deviceinfo": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c MAC-\u0430\u0434\u0440\u0435\u0441. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/tr.json b/homeassistant/components/anthemav/translations/tr.json new file mode 100644 index 00000000000..cbe85a5319c --- /dev/null +++ b/homeassistant/components/anthemav/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "cannot_receive_deviceinfo": "MAC Adresi al\u0131namad\u0131. Cihaz\u0131n a\u00e7\u0131k oldu\u011fundan emin olun" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json new file mode 100644 index 00000000000..0751331f82f --- /dev/null +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "cannot_receive_deviceinfo": "\u63a5\u6536 MAC \u4f4d\u5740\u5931\u6557\uff0c\u8acb\u78ba\u5b9a\u88dd\u7f6e\u70ba\u958b\u555f\u72c0Address. Make sure the device is turned on" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 5177c6f3486..5d9c1cde785 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -67,17 +67,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) ) - async def setup_platforms(): - """Set up platforms and initiate connection.""" - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - await manager.init() - - hass.async_create_task(setup_platforms()) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await manager.init() return True diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index dec195fddee..5717f851b81 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.10.2"], + "requirements": ["pyatv==0.10.3"], "dependencies": ["zeroconf"], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 4c3a8cdee94..ae1e55bf3ba 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -5,8 +5,8 @@ "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "backoff": "Az eszk\u00f6z jelenleg nem fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmeket (lehet, hogy t\u00fal sokszor adott meg \u00e9rv\u00e9nytelen PIN-k\u00f3dot), pr\u00f3b\u00e1lkozzon \u00fajra k\u00e9s\u0151bb.", "device_did_not_pair": "A p\u00e1ros\u00edt\u00e1s folyamat\u00e1t az eszk\u00f6zr\u0151l nem pr\u00f3b\u00e1lt\u00e1k befejezni.", - "device_not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a felder\u00edt\u00e9s sor\u00e1n, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", - "inconsistent_device": "Az elv\u00e1rt protokollok nem tal\u00e1lhat\u00f3k a felder\u00edt\u00e9s sor\u00e1n. Ez \u00e1ltal\u00e1ban a multicast DNS (Zeroconf) probl\u00e9m\u00e1j\u00e1t jelzi. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni az eszk\u00f6zt.", + "device_not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a felder\u00edt\u00e9s sor\u00e1n, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", + "inconsistent_device": "Az elv\u00e1rt protokollok nem tal\u00e1lhat\u00f3k a felder\u00edt\u00e9s sor\u00e1n. Ez \u00e1ltal\u00e1ban a multicast DNS (Zeroconf) probl\u00e9m\u00e1j\u00e1t jelzi. K\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni az eszk\u00f6zt.", "ipv6_not_supported": "Az IPv6 nem t\u00e1mogatott.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", @@ -22,26 +22,26 @@ "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "\u00d6n a `{name}`, `{type}` t\u00edpus\u00fa eszk\u00f6zt k\u00e9sz\u00fcl hozz\u00e1adni a Home Assistanthoz.\n\n**A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN k\u00f3dot is meg kell adnia.**\n\nK\u00e9rj\u00fck, vegye figyelembe, hogy ezzel az integr\u00e1ci\u00f3val *nem* tudja kikapcsolni az Apple TV k\u00e9sz\u00fcl\u00e9k\u00e9t. Csak a Home Assistant m\u00e9dialej\u00e1tsz\u00f3ja fog kikapcsolni!", + "description": "\u00d6n a `{name}`, `{type}` t\u00edpus\u00fa eszk\u00f6zt k\u00e9sz\u00fcl hozz\u00e1adni a Home Assistanthoz.\n\n**A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN k\u00f3dot is meg kell adnia.**\n\nK\u00e9rem, vegye figyelembe, hogy ezzel az integr\u00e1ci\u00f3val *nem* tudja kikapcsolni az Apple TV k\u00e9sz\u00fcl\u00e9k\u00e9t. Csak a Home Assistant m\u00e9dialej\u00e1tsz\u00f3ja fog kikapcsolni!", "title": "Apple TV sikeresen hozz\u00e1adva" }, "pair_no_pin": { - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rj\u00fck, \u00edrja be k\u00e9sz\u00fcl\u00e9ken a PIN k\u00f3dot: {pin}.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rem, \u00edrja be k\u00e9sz\u00fcl\u00e9ken a PIN k\u00f3dot: {pin}.", "title": "P\u00e1ros\u00edt\u00e1s" }, "pair_with_pin": { "data": { "pin": "PIN-k\u00f3d" }, - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} protokollhoz. K\u00e9rem, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", "title": "P\u00e1ros\u00edt\u00e1s" }, "password": { - "description": "`{protocol}` jelsz\u00f3t ig\u00e9nyel. Ez m\u00e9g nem t\u00e1mogatott, k\u00e9rj\u00fck, a folytat\u00e1shoz tiltsa le a jelsz\u00f3t.", + "description": "`{protocol}` jelsz\u00f3t ig\u00e9nyel. Ez m\u00e9g nem t\u00e1mogatott, k\u00e9rem, a folytat\u00e1shoz tiltsa le a jelsz\u00f3t.", "title": "Jelsz\u00f3 sz\u00fcks\u00e9ges" }, "protocol_disabled": { - "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protokoll}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rj\u00fck, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", + "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protocol}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rem, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", "title": "A p\u00e1ros\u00edt\u00e1s nem lehets\u00e9ges" }, "reconfigure": { diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index 3661cbeedb3..860bf4c961b 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -22,7 +22,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eApple TV\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002 \n\n **\u51e6\u7406\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u8907\u6570\u306ePIN\u30b3\u30fc\u30c9\u306e\u5165\u529b\u304c\u5fc5\u8981\u306b\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002** \n\n\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Apple TV\u306e\u96fb\u6e90\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u306f *\u3067\u304d\u306a\u3044* \u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002 Home Assistant\u306e\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u307f\u304c\u30aa\u30d5\u306b\u306a\u308a\u307e\u3059\uff01", + "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eApple TV\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002 \n\n **\u51e6\u7406\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u8907\u6570\u306ePIN\u30b3\u30fc\u30c9\u306e\u5165\u529b\u304c\u5fc5\u8981\u306b\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002** \n\n\u3053\u306e\u7d71\u5408\u3067\u306f\u3001Apple TV\u306e\u96fb\u6e90\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u306f *\u3067\u304d\u306a\u3044* \u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002 Home Assistant\u306e\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u307f\u304c\u30aa\u30d5\u306b\u306a\u308a\u307e\u3059\uff01", "title": "Apple TV\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" }, "pair_no_pin": { diff --git a/homeassistant/components/apple_tv/translations/pt.json b/homeassistant/components/apple_tv/translations/pt.json index deec60a19ea..e54e421caa5 100644 --- a/homeassistant/components/apple_tv/translations/pt.json +++ b/homeassistant/components/apple_tv/translations/pt.json @@ -40,7 +40,7 @@ "data": { "device_input": "Dispositivo" }, - "description": "Comece por introduzir o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que pretende adicionar. Se algum dispositivo foi automaticamente encontrado na sua rede, ele \u00e9 mostrado abaixo.\n\nSe n\u00e3o conseguir ver o seu dispositivo ou se tiver algum problema, tente especificar o endere\u00e7o IP do dispositivo.\n\n{devices}", + "description": "Comece por introduzir o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que pretende adicionar. Se algum dispositivo foi automaticamente encontrado na sua rede, ele \u00e9 mostrado abaixo.\n\nSe n\u00e3o conseguir ver o seu dispositivo ou se tiver algum problema, tente especificar o endere\u00e7o IP do dispositivo.", "title": "Configure uma nova Apple TV" } } diff --git a/homeassistant/components/application_credentials/translations/cs.json b/homeassistant/components/application_credentials/translations/cs.json new file mode 100644 index 00000000000..6699bb0a908 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "P\u0159ihla\u0161ovac\u00ed \u00fadaje aplikace" +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index a82e0239842..c2c6be0db30 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL)) tasks[entry.entry_id] = task - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index 897b462eb48..0e693857363 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -19,7 +19,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t" + "description": "K\u00e9rem, adja meg az eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t" } } }, diff --git a/homeassistant/components/aseko_pool_live/__init__.py b/homeassistant/components/aseko_pool_live/__init__.py index 213d0dabc91..70a66251bdc 100644 --- a/homeassistant/components/aseko_pool_live/__init__.py +++ b/homeassistant/components/aseko_pool_live/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id].append((unit, coordinator)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aseko_pool_live/translations/pt.json b/homeassistant/components/aseko_pool_live/translations/pt.json new file mode 100644 index 00000000000..2933743c867 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 0532dd9c7cc..f3d12c3bd39 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ASUSWRT: router} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/asuswrt/translations/bg.json b/homeassistant/components/asuswrt/translations/bg.json index df452e48980..a407de4c584 100644 --- a/homeassistant/components/asuswrt/translations/bg.json +++ b/homeassistant/components/asuswrt/translations/bg.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json index 34581ec6b7a..3627f1f22eb 100644 --- a/homeassistant/components/asuswrt/translations/hu.json +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -8,7 +8,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", "pwd_and_ssh": "Csak jelsz\u00f3 vagy SSH kulcsf\u00e1jlt adjon meg", - "pwd_or_ssh": "K\u00e9rj\u00fck, adja meg a jelsz\u00f3t vagy az SSH kulcsf\u00e1jlt", + "pwd_or_ssh": "K\u00e9rem, adja meg a jelsz\u00f3t vagy az SSH kulcsf\u00e1jlt", "ssh_not_file": "Az SSH kulcsf\u00e1jl nem tal\u00e1lhat\u00f3", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/asuswrt/translations/pt.json b/homeassistant/components/asuswrt/translations/pt.json new file mode 100644 index 00000000000..54c86ef332a --- /dev/null +++ b/homeassistant/components/asuswrt/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta (leave empty for protocol default)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 6eb17fded3f..56b2c1e969e 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=atag.id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index e8df7e1072d..81842f995e8 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -88,7 +88,7 @@ async def async_setup_august( data = hass.data[DOMAIN][config_entry.entry_id] = AugustData(hass, august_gateway) await data.async_setup() - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index 42f9860bdc2..68953d8b888 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -30,7 +30,7 @@ "data": { "code": "Ellen\u0151rz\u0151 k\u00f3d" }, - "description": "K\u00e9rj\u00fck, ellen\u0151rizze a {login_method} ({username}), \u00e9s \u00edrja be al\u00e1bb az ellen\u0151rz\u0151 k\u00f3dot", + "description": "K\u00e9rem, ellen\u0151rizze a {login_method} ({username}), \u00e9s \u00edrja be al\u00e1bb az ellen\u0151rz\u0151 k\u00f3dot", "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" } } diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 01d6092a4f2..b8d0589d007 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: AURORA_API: api, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aurora/translations/sv.json b/homeassistant/components/aurora/translations/sv.json new file mode 100644 index 00000000000..7e16a2c036e --- /dev/null +++ b/homeassistant/components/aurora/translations/sv.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "threshold": "Gr\u00e4nsv\u00e4rde (%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index c988121b6bd..305a42d4dcc 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.data[CONF_ADDRESS] ser_client = AuroraSerialClient(address, comport, parity="N", timeout=1) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ser_client - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aurora_abb_powerone/translations/pt.json b/homeassistant/components/aurora_abb_powerone/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index 6136ff6f8f3..b32e11f27c0 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "client": client, "services": services, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py index 09946cef03d..648abaccb0c 100644 --- a/homeassistant/components/aussie_broadband/sensor.py +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +import re from typing import Any, cast from homeassistant.components.sensor import ( @@ -33,7 +34,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Internet Services sensors SensorValueEntityDescription( key="usedMb", - name="Data Used", + name="Data used", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:network", @@ -55,35 +56,35 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Mobile Phone Services sensors SensorValueEntityDescription( key="national", - name="National Calls", + name="National calls", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="mobile", - name="Mobile Calls", + name="Mobile calls", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="international", - name="International Calls", + name="International calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone-plus", ), SensorValueEntityDescription( key="sms", - name="SMS Sent", + name="SMS sent", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:message-processing", value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="internet", - name="Data Used", + name="Data used", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_KILOBYTES, icon="mdi:network", @@ -91,14 +92,14 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( ), SensorValueEntityDescription( key="voicemail", - name="Voicemail Calls", + name="Voicemail calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", ), SensorValueEntityDescription( key="other", - name="Other Calls", + name="Other calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", @@ -106,13 +107,13 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Generic sensors SensorValueEntityDescription( key="daysTotal", - name="Billing Cycle Length", + name="Billing cycle length", native_unit_of_measurement=TIME_DAYS, icon="mdi:calendar-range", ), SensorValueEntityDescription( key="daysRemaining", - name="Billing Cycle Remaining", + name="Billing cycle remaining", native_unit_of_measurement=TIME_DAYS, icon="mdi:calendar-clock", ), @@ -137,6 +138,7 @@ async def async_setup_entry( class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): """Base class for Aussie Broadband metric sensors.""" + _attr_has_entity_name = True entity_description: SensorValueEntityDescription def __init__( @@ -146,13 +148,12 @@ class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): super().__init__(service["coordinator"]) self.entity_description = description self._attr_unique_id = f"{service[SERVICE_ID]}:{description.key}" - self._attr_name = f"{service['name']} {description.name}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, service[SERVICE_ID])}, manufacturer="Aussie Broadband", configuration_url=f"https://my.aussiebroadband.com.au/#/{service['name'].lower()}/{service[SERVICE_ID]}/", - name=service["description"], + name=re.sub(r" - AVC\d+$", "", service["description"]), model=service["name"], ) diff --git a/homeassistant/components/aussie_broadband/translations/ja.json b/homeassistant/components/aussie_broadband/translations/ja.json index d8351bbc98b..fb2f908851a 100644 --- a/homeassistant/components/aussie_broadband/translations/ja.json +++ b/homeassistant/components/aussie_broadband/translations/ja.json @@ -16,7 +16,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "service": { "data": { diff --git a/homeassistant/components/aussie_broadband/translations/pt.json b/homeassistant/components/aussie_broadband/translations/pt.json new file mode 100644 index 00000000000..5a8312a5ac1 --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + }, + "options": { + "abort": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index b24da92afdd..6cc9d94c7a6 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -257,7 +257,7 @@ class LoginFlowResourceView(LoginFlowBaseView): @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA)) @log_invalid_auth - async def post(self, request, flow_id, data): + async def post(self, request, data, flow_id): """Handle progressing a login flow request.""" client_id = data.pop("client_id") diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index 2364b85a63e..ef39e488001 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 5c2a3da89ce..6fcf63abb4d 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -86,7 +86,7 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( ), AwairSensorEntityDescription( key=API_VOC, - icon="mdi:cloud", + icon="mdi:molecule", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, name="Volatile organic compounds", unique_id_tag="VOC", # matches legacy format @@ -101,7 +101,6 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( AwairSensorEntityDescription( key=API_CO2, device_class=SensorDeviceClass.CO2, - icon="mdi:cloud", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, name="Carbon dioxide", unique_id_tag="CO2", # matches legacy format @@ -111,14 +110,14 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = ( AwairSensorEntityDescription( key=API_PM25, - icon="mdi:blur", + device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name="PM2.5", unique_id_tag="PM25", # matches legacy format ), AwairSensorEntityDescription( key=API_PM10, - icon="mdi:blur", + device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name="PM10", unique_id_tag="PM10", # matches legacy format diff --git a/homeassistant/components/awair/translations/ar.json b/homeassistant/components/awair/translations/ar.json new file mode 100644 index 00000000000..3f25b63e7b9 --- /dev/null +++ b/homeassistant/components/awair/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "email": "\u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 0acefe23c02..e878c370d93 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -17,6 +17,13 @@ }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair." }, + "reauth_confirm": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "email": "Email" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair." + }, "user": { "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 2c6fab90909..863b7982b2a 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -17,6 +17,13 @@ }, "description": "Masukkan kembali token akses pengembang Awair Anda." }, + "reauth_confirm": { + "data": { + "access_token": "Token Akses", + "email": "Email" + }, + "description": "Masukkan kembali token akses pengembang Awair Anda." + }, "user": { "data": { "access_token": "Token Akses", diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 83121c9fe42..7c7b73b312f 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -17,6 +17,13 @@ }, "description": "Awair developer access token\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "reauth_confirm": { + "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "email": "E\u30e1\u30fc\u30eb" + }, + "description": "Awair developer access token\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index 38bd24f714b..b0ee9e85adf 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -17,6 +17,13 @@ }, "description": "Wprowad\u017a ponownie token dost\u0119pu programisty Awair." }, + "reauth_confirm": { + "data": { + "access_token": "Token dost\u0119pu", + "email": "Adres e-mail" + }, + "description": "Wprowad\u017a ponownie token dost\u0119pu programisty Awair." + }, "user": { "data": { "access_token": "Token dost\u0119pu", diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index ea99bbf0167..c906e6f380e 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -16,6 +16,12 @@ "email": "Email" } }, + "reauth_confirm": { + "data": { + "access_token": "Token de Acesso", + "email": "Email" + } + }, "user": { "data": { "access_token": "Token de Acesso", diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index 05a14ce7857..23424091565 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -17,6 +17,13 @@ }, "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." }, + "reauth_confirm": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." + }, "user": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index e8fa8ca1027..ef34620b5f7 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -17,6 +17,13 @@ }, "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." }, + "reauth_confirm": { + "data": { + "access_token": "Eri\u015fim Anahtar\u0131", + "email": "E-posta" + }, + "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." + }, "user": { "data": { "access_token": "Eri\u015fim Anahtar\u0131", diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index 0bd7749c65f..f14acef8550 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -17,6 +17,13 @@ }, "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" }, + "reauth_confirm": { + "data": { + "access_token": "\u5b58\u53d6\u6b0a\u6756", + "email": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" + }, "user": { "data": { "access_token": "\u5b58\u53d6\u6b0a\u6756", diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 5e211c00028..4af066f4e89 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -4,28 +4,36 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.entity_registry import async_migrate_entries -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice +from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS +from .device import AxisNetworkDevice, get_axis_device +from .errors import AuthenticationRequired, CannotConnect _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Set up the Axis component.""" + """Set up the Axis integration.""" hass.data.setdefault(AXIS_DOMAIN, {}) - device = AxisNetworkDevice(hass, config_entry) - - if not await device.async_setup(): - return False - - hass.data[AXIS_DOMAIN][config_entry.unique_id] = device + try: + api = await get_axis_device(hass, config_entry.data) + except CannotConnect as err: + raise ConfigEntryNotReady from err + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] = AxisNetworkDevice( + hass, config_entry, api + ) await device.async_update_device_registry() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + device.async_setup_events() + config_entry.add_update_listener(device.async_new_address_callback) config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) ) diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 791764dc605..3d887478528 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -10,6 +10,8 @@ from .const import DOMAIN as AXIS_DOMAIN class AxisEntityBase(Entity): """Base common to all Axis entities.""" + _attr_has_entity_name = True + def __init__(self, device): """Initialize the Axis event.""" self.device = device @@ -47,7 +49,7 @@ class AxisEventBase(AxisEntityBase): super().__init__(device) self.event = event - self._attr_name = f"{device.name} {event.TYPE} {event.id}" + self._attr_name = f"{event.TYPE} {event.id}" self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}" self._attr_device_class = event.CLASS diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index d2b6a9f7dd7..fba7e8d6248 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -110,9 +110,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): and self.event.id in self.device.api.vapix.ports and self.device.api.vapix.ports[self.event.id].name ): - return ( - f"{self.device.name} {self.device.api.vapix.ports[self.event.id].name}" - ) + return self.device.api.vapix.ports[self.event.id].name if self.event.CLASS == CLASS_MOTION: @@ -128,6 +126,6 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): and event_data and self.event.id in event_data ): - return f"{self.device.name} {self.event.TYPE} {event_data[self.event.id].name}" + return f"{self.event.TYPE} {event_data[self.event.id].name}" return self._attr_name diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index bd46ae54f81..4df9a5e2141 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -40,7 +40,6 @@ class AxisCamera(AxisEntityBase, MjpegCamera): MjpegCamera.__init__( self, - name=device.name, username=device.username, password=device.password, mjpeg_url=self.mjpeg_source, diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index f94c27dc2ac..1ce2f08c045 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping from ipaddress import ip_address +from types import MappingProxyType from typing import Any from urllib.parse import urlsplit @@ -32,7 +33,7 @@ from .const import ( DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) -from .device import AxisNetworkDevice, get_device +from .device import AxisNetworkDevice, get_axis_device from .errors import AuthenticationRequired, CannotConnect AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} @@ -66,13 +67,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): if user_input is not None: try: - device = await get_device( - self.hass, - host=user_input[CONF_HOST], - port=user_input[CONF_PORT], - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - ) + device = await get_axis_device(self.hass, MappingProxyType(user_input)) serial = device.vapix.serial_number await self.async_set_unique_id(format_mac(serial)) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index d0d5e230d2f..683991d0f65 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,6 +1,8 @@ """Axis network device abstraction.""" import asyncio +from types import MappingProxyType +from typing import Any import async_timeout import axis @@ -24,7 +26,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -50,15 +51,15 @@ from .errors import AuthenticationRequired, CannotConnect class AxisNetworkDevice: """Manages a Axis device.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry, api): """Initialize the device.""" self.hass = hass self.config_entry = config_entry - self.available = True + self.api = api - self.api = None - self.fw_version = None - self.product_type = None + self.available = True + self.fw_version = api.vapix.firmware_version + self.product_type = api.vapix.product_type @property def host(self): @@ -184,7 +185,7 @@ class AxisNetworkDevice: sw_version=self.fw_version, ) - async def use_mqtt(self, hass: HomeAssistant, component: str) -> None: + async def async_use_mqtt(self, hass: HomeAssistant, component: str) -> None: """Set up to use MQTT.""" try: status = await self.api.vapix.mqtt.get_client_status() @@ -209,50 +210,18 @@ class AxisNetworkDevice: # Setup and teardown methods - async def async_setup(self): - """Set up the device.""" - try: - self.api = await get_device( - self.hass, - host=self.host, - port=self.port, - username=self.username, - password=self.password, + def async_setup_events(self): + """Set up the device events.""" + + if self.option_events: + self.api.stream.connection_status_callback.append( + self.async_connection_status_callback ) + self.api.enable_events(event_callback=self.async_event_callback) + self.api.stream.start() - except CannotConnect as err: - raise ConfigEntryNotReady from err - - except AuthenticationRequired as err: - raise ConfigEntryAuthFailed from err - - self.fw_version = self.api.vapix.firmware_version - self.product_type = self.api.vapix.product_type - - async def start_platforms(): - await asyncio.gather( - *( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - for platform in PLATFORMS - ) - ) - if self.option_events: - self.api.stream.connection_status_callback.append( - self.async_connection_status_callback - ) - self.api.enable_events(event_callback=self.async_event_callback) - self.api.stream.start() - - if self.api.vapix.mqtt: - async_when_setup(self.hass, MQTT_DOMAIN, self.use_mqtt) - - self.hass.async_create_task(start_platforms()) - - self.config_entry.add_update_listener(self.async_new_address_callback) - - return True + if self.api.vapix.mqtt: + async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) @callback def disconnect_from_stream(self): @@ -274,14 +243,21 @@ class AxisNetworkDevice: ) -async def get_device( - hass: HomeAssistant, host: str, port: int, username: str, password: str +async def get_axis_device( + hass: HomeAssistant, + config: MappingProxyType[str, Any], ) -> axis.AxisDevice: """Create a Axis device.""" session = get_async_client(hass, verify_ssl=False) device = axis.AxisDevice( - Configuration(session, host, port=port, username=username, password=password) + Configuration( + session, + config[CONF_HOST], + port=config[CONF_PORT], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + ) ) try: @@ -291,11 +267,13 @@ async def get_device( return device except axis.Unauthorized as err: - LOGGER.warning("Connected to device at %s but not registered", host) + LOGGER.warning( + "Connected to device at %s but not registered", config[CONF_HOST] + ) raise AuthenticationRequired from err except (asyncio.TimeoutError, axis.RequestError) as err: - LOGGER.error("Error connecting to the Axis device at %s", host) + LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) raise CannotConnect from err except axis.AxisException as err: diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index 30f1cee9340..e34c0d4a2d6 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -53,7 +53,7 @@ class AxisLight(AxisEventBase, LightEntity): self.max_intensity = 0 light_type = device.api.vapix.light_control[self.light_id].light_type - self._attr_name = f"{device.name} {light_type} {event.TYPE} {event.id}" + self._attr_name = f"{light_type} {event.TYPE} {event.id}" self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_color_mode = ColorMode.BRIGHTNESS diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index a778c974737..61f16cfc789 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -40,7 +40,7 @@ class AxisSwitch(AxisEventBase, SwitchEntity): super().__init__(event, device) if event.id and device.api.vapix.ports[event.id].name: - self._attr_name = f"{device.name} {device.api.vapix.ports[event.id].name}" + self._attr_name = device.api.vapix.ports[event.id].name @property def is_on(self): diff --git a/homeassistant/components/axis/translations/pt.json b/homeassistant/components/axis/translations/pt.json index 8ba642263a4..b74ecb2dc44 100644 --- a/homeassistant/components/axis/translations/pt.json +++ b/homeassistant/components/axis/translations/pt.json @@ -10,6 +10,7 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index c6de8515979..645932f8b73 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator, project - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json index 2d8879b9d68..ed0dd7f4d1e 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -15,7 +15,7 @@ "data": { "personal_access_token": "Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token (PAT)" }, - "description": "{project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", + "description": "{project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rem, adja meg az aktu\u00e1lis hiteles\u00edt\u0151 adatait.", "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/azure_devops/translations/pt.json b/homeassistant/components/azure_devops/translations/pt.json index 2af1f548447..b09f2cceda7 100644 --- a/homeassistant/components/azure_devops/translations/pt.json +++ b/homeassistant/components/azure_devops/translations/pt.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" - } + }, + "flow_title": "{project_url}" } } \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/hu.json b/homeassistant/components/azure_event_hub/translations/hu.json index 21b6a4ba63f..e0c50a4ae56 100644 --- a/homeassistant/components/azure_event_hub/translations/hu.json +++ b/homeassistant/components/azure_event_hub/translations/hu.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "cannot_connect": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz.", + "cannot_connect": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rem, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "unknown": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz." + "unknown": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rem, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -15,7 +15,7 @@ "data": { "event_hub_connection_string": "Event Hub csatlakoz\u00e1si karaktersor" }, - "description": "K\u00e9rj\u00fck, adja meg a csatlakoz\u00e1si karaktersort ehhez: {event_hub_instance_name}", + "description": "K\u00e9rem, adja meg a csatlakoz\u00e1si karaktersort ehhez: {event_hub_instance_name}", "title": "Csatlakoz\u00e1si karaktersor t\u00edpus" }, "sas": { @@ -24,7 +24,7 @@ "event_hub_sas_key": "Event Hub SAS kulcs", "event_hub_sas_policy": "Event Hub SAS h\u00e1zirend" }, - "description": "K\u00e9rj\u00fck, adja meg a SAS hiteles\u00edt\u0151 adatait ehhez: {event_hub_instance_name}", + "description": "K\u00e9rem, adja meg a SAS hiteles\u00edt\u0151 adatait ehhez: {event_hub_instance_name}", "title": "SAS hiteles\u00edt\u00e9s t\u00edpus" }, "user": { diff --git a/homeassistant/components/azure_event_hub/translations/ja.json b/homeassistant/components/azure_event_hub/translations/ja.json index d8c0407fbc5..720e57d8066 100644 --- a/homeassistant/components/azure_event_hub/translations/ja.json +++ b/homeassistant/components/azure_event_hub/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3067\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { @@ -32,7 +32,7 @@ "event_hub_instance_name": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u540d", "use_connection_string": "\u63a5\u7d9a\u6587\u5b57\u5217\u3092\u4f7f\u7528\u3059\u308b" }, - "title": "Azure Event Hub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Azure Event Hub\u7d71\u5408\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, diff --git a/homeassistant/components/azure_event_hub/translations/pt.json b/homeassistant/components/azure_event_hub/translations/pt.json new file mode 100644 index 00000000000..d252c078a2c --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json index 6cf5e2bf406..26ccea446f2 100644 --- a/homeassistant/components/azure_service_bus/manifest.json +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -2,7 +2,7 @@ "domain": "azure_service_bus", "name": "Azure Service Bus", "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", - "requirements": ["azure-servicebus==0.50.3"], + "requirements": ["azure-servicebus==7.8.0"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_push", "loggers": ["azure"] diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 0d48ff6b2d6..53873373011 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -2,11 +2,12 @@ import json import logging -from azure.servicebus.aio import Message, ServiceBusClient -from azure.servicebus.common.errors import ( - MessageSendFailed, +from azure.servicebus import ServiceBusMessage +from azure.servicebus.aio import ServiceBusClient +from azure.servicebus.exceptions import ( + MessagingEntityNotFoundError, ServiceBusConnectionError, - ServiceBusResourceNotFound, + ServiceBusError, ) import voluptuous as vol @@ -60,10 +61,10 @@ def get_service(hass, config, discovery_info=None): try: if queue_name: - client = servicebus.get_queue(queue_name) + client = servicebus.get_queue_sender(queue_name) else: - client = servicebus.get_topic(topic_name) - except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err: + client = servicebus.get_topic_sender(topic_name) + except (ServiceBusConnectionError, MessagingEntityNotFoundError) as err: _LOGGER.error( "Connection error while creating client for queue/topic '%s'. %s", queue_name or topic_name, @@ -93,11 +94,12 @@ class ServiceBusNotificationService(BaseNotificationService): if data := kwargs.get(ATTR_DATA): dto.update(data) - queue_message = Message(json.dumps(dto)) - queue_message.properties.content_type = CONTENT_TYPE_JSON + queue_message = ServiceBusMessage( + json.dumps(dto), content_type=CONTENT_TYPE_JSON + ) try: - await self._client.send(queue_message) - except MessageSendFailed as err: + await self._client.send_messages(queue_message) + except ServiceBusError as err: _LOGGER.error( "Could not send service bus notification to %s. %s", self._client.name, diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 601215d4c61..7e80341deab 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -15,6 +15,7 @@ from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.FAN, Platform.LIGHT, @@ -39,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_future) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/baf/binary_sensor.py b/homeassistant/components/baf/binary_sensor.py new file mode 100644 index 00000000000..8a00a7e8bd5 --- /dev/null +++ b/homeassistant/components/baf/binary_sensor.py @@ -0,0 +1,80 @@ +"""Support for Big Ass Fans binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Optional, cast + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BAFEntity +from .models import BAFData + + +@dataclass +class BAFBinarySensorDescriptionMixin: + """Required values for BAF binary sensors.""" + + value_fn: Callable[[Device], bool | None] + + +@dataclass +class BAFBinarySensorDescription( + BinarySensorEntityDescription, + BAFBinarySensorDescriptionMixin, +): + """Class describing BAF binary sensor entities.""" + + +OCCUPANCY_SENSORS = ( + BAFBinarySensorDescription( + key="occupancy", + name="Occupancy", + device_class=BinarySensorDeviceClass.OCCUPANCY, + value_fn=lambda device: cast(Optional[bool], device.fan_occupancy_detected), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF binary sensors.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + sensors_descriptions: list[BAFBinarySensorDescription] = [] + if device.has_occupancy: + sensors_descriptions.extend(OCCUPANCY_SENSORS) + async_add_entities( + BAFBinarySensor(device, description) for description in sensors_descriptions + ) + + +class BAFBinarySensor(BAFEntity, BinarySensorEntity): + """BAF binary sensor.""" + + entity_description: BAFBinarySensorDescription + + def __init__(self, device: Device, description: BAFBinarySensorDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, f"{device.name} {description.name}") + self._attr_unique_id = f"{self._device.mac_address}-{description.key}" + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + description = self.entity_description + self._attr_is_on = description.value_fn(self._device) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 821ad1a21cb..15e0272b2b0 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.6.0"], + "requirements": ["aiobafi6==0.7.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/homeassistant/components/baf/translations/pt.json b/homeassistant/components/baf/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/baf/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py index 60989ecc6d6..6be1d741137 100644 --- a/homeassistant/components/balboa/__init__.py +++ b/homeassistant/components/balboa/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(stop_monitoring) # At this point we have a configured spa. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def keep_alive(now: datetime) -> None: """Keep alive task.""" diff --git a/homeassistant/components/balboa/translations/pt.json b/homeassistant/components/balboa/translations/pt.json new file mode 100644 index 00000000000..f13cad90edc --- /dev/null +++ b/homeassistant/components/balboa/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant/components/binary_sensor/translations/cs.json index b8793a2c087..9e5c5b9d7c2 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -97,6 +97,10 @@ "vibration": "{entity_name} za\u010dalo detekovat vibrace" } }, + "device_class": { + "gas": "plyn", + "sound": "zvuk" + }, "state": { "_": { "off": "Neaktivn\u00ed", diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index 9d7fdda1006..9eba64372d4 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -89,6 +89,10 @@ "vibration": "foram detectadas vibra\u00e7\u00f5es em {entity_name}" } }, + "device_class": { + "moisture": "humidade", + "problem": "problema" + }, "state": { "_": { "off": "Desligado", @@ -102,6 +106,9 @@ "off": "Sem carregar", "on": "A carregar" }, + "carbon_monoxide": { + "on": "Detectado" + }, "cold": { "off": "Normal", "on": "Frio" @@ -178,9 +185,13 @@ "off": "Limpo", "on": "Detectado" }, + "update": { + "off": "Actualizado", + "on": "Atualiza\u00e7\u00e3o dispon\u00edvel" + }, "vibration": { "off": "Limpo", - "on": "Detetado" + "on": "Detectado" }, "window": { "off": "Fechada", diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index a8f93dd0122..ff907d728b7 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -17,12 +17,13 @@ from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT _LOGGER = logging.getLogger(__name__) PLATFORMS = [ + Platform.AIR_QUALITY, + Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, + Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, - Platform.AIR_QUALITY, - Platform.LIGHT, - Platform.CLIMATE, ] PARALLEL_UPDATES = 0 @@ -48,7 +49,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: domain_entry = domain.setdefault(entry.entry_id, {}) product = domain_entry.setdefault(PRODUCT, product) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py new file mode 100644 index 00000000000..e9ceaac2dc7 --- /dev/null +++ b/homeassistant/components/blebox/button.py @@ -0,0 +1,47 @@ +"""BleBox button entities implementation.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import BleBoxEntity, create_blebox_entities + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a BleBox button entry.""" + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxButtonEntity, "buttons" + ) + + +class BleBoxButtonEntity(BleBoxEntity, ButtonEntity): + """Representation of BleBox buttons.""" + + def __init__(self, feature): + """Initialize a BleBox button feature.""" + super().__init__(feature) + self._attr_icon = self.get_icon() + + def get_icon(self): + """Return icon for endpoint.""" + if "up" in self._feature.query_string: + return "mdi:arrow-up-circle" + if "down" in self._feature.query_string: + return "mdi:arrow-down-circle" + if "fav" in self._feature.query_string: + return "mdi:heart-circle" + if "open" in self._feature.query_string: + return "mdi:arrow-up-circle" + if "close" in self._feature.query_string: + return "mdi:arrow-down-circle" + return "" + + async def async_press(self) -> None: + """Handle the button press.""" + await self._feature.set() diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index 013a6501068..533c33f37bb 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -1,9 +1,5 @@ """Constants for the BleBox devices integration.""" -from homeassistant.components.cover import CoverDeviceClass -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.components.switch import SwitchDeviceClass -from homeassistant.const import TEMP_CELSIUS DOMAIN = "blebox" PRODUCT = "product" @@ -16,16 +12,6 @@ CANNOT_CONNECT = "cannot_connect" UNSUPPORTED_VERSION = "unsupported_version" UNKNOWN = "unknown" -BLEBOX_TO_HASS_DEVICE_CLASSES = { - "shutter": CoverDeviceClass.SHUTTER, - "gatebox": CoverDeviceClass.DOOR, - "gate": CoverDeviceClass.GATE, - "relay": SwitchDeviceClass.SWITCH, - "temperature": SensorDeviceClass.TEMPERATURE, -} - - -BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} DEFAULT_HOST = "192.168.0.2" DEFAULT_PORT = 80 diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 8763ec34d34..882356f1a77 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -5,6 +5,7 @@ from typing import Any from homeassistant.components.cover import ( ATTR_POSITION, + CoverDeviceClass, CoverEntity, CoverEntityFeature, ) @@ -14,7 +15,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_DEVICE_CLASSES + +BLEBOX_TO_COVER_DEVICE_CLASSES = { + "gate": CoverDeviceClass.GATE, + "gatebox": CoverDeviceClass.DOOR, + "shutter": CoverDeviceClass.SHUTTER, +} + BLEBOX_TO_HASS_COVER_STATES = { None: None, @@ -49,7 +56,7 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): def __init__(self, feature): """Initialize a BleBox cover feature.""" super().__init__(feature) - self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + self._attr_device_class = BLEBOX_TO_COVER_DEVICE_CLASSES[feature.device_class] position = CoverEntityFeature.SET_POSITION if feature.is_slider else 0 stop = CoverEntityFeature.STOP if feature.has_stop else 0 self._attr_supported_features = ( diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 08554695316..49d44db8f01 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.0.1"], + "requirements": ["blebox_uniapi==2.0.2"], "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 2c317acada8..663af970e3e 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -1,11 +1,15 @@ """BleBox sensor entities.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_DEVICE_CLASSES, BLEBOX_TO_UNIT_MAP + +BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} + +BLEBOX_TO_SENSOR_DEVICE_CLASS = {"temperature": SensorDeviceClass.TEMPERATURE} async def async_setup_entry( @@ -27,7 +31,7 @@ class BleBoxSensorEntity(BleBoxEntity, SensorEntity): """Initialize a BleBox sensor feature.""" super().__init__(feature) self._attr_native_unit_of_measurement = BLEBOX_TO_UNIT_MAP[feature.unit] - self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + self._attr_device_class = BLEBOX_TO_SENSOR_DEVICE_CLASS[feature.device_class] @property def native_value(self): diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index 50eba1d2c4a..f9c866244c7 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -1,13 +1,12 @@ """BleBox switch implementation.""" from datetime import timedelta -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_DEVICE_CLASSES SCAN_INTERVAL = timedelta(seconds=5) @@ -29,7 +28,7 @@ class BleBoxSwitchEntity(BleBoxEntity, SwitchEntity): def __init__(self, feature): """Initialize a BleBox switch feature.""" super().__init__(feature) - self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + self._attr_device_class = SwitchDeviceClass.SWITCH @property def is_on(self): diff --git a/homeassistant/components/blebox/translations/pt.json b/homeassistant/components/blebox/translations/pt.json index 9c2be6fd04b..8b581a984e7 100644 --- a/homeassistant/components/blebox/translations/pt.json +++ b/homeassistant/components/blebox/translations/pt.json @@ -8,6 +8,7 @@ "unknown": "Erro inesperado", "unsupported_version": "O dispositivo BleBox possui firmware desatualizado. Atualize-o primeiro." }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 7321e2392df..0c18950da66 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.data[DOMAIN][entry.entry_id].available: raise ConfigEntryNotReady - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def blink_refresh(event_time=None): """Call blink to refresh info.""" diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json index 40724c01d42..0ff7c2dc1b4 100644 --- a/homeassistant/components/blink/translations/ja.json +++ b/homeassistant/components/blink/translations/ja.json @@ -32,7 +32,7 @@ "data": { "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" }, - "description": "Blink\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a", + "description": "Blink\u7d71\u5408\u306e\u8a2d\u5b9a", "title": "Blink \u30aa\u30d7\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/blink/translations/pt.json b/homeassistant/components/blink/translations/pt.json index 76c420a584c..1f71ecf3f22 100644 --- a/homeassistant/components/blink/translations/pt.json +++ b/homeassistant/components/blink/translations/pt.json @@ -6,7 +6,8 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_access_token": "Token de acesso inv\u00e1lido", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "step": { "2fa": { diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index 4b14201652f..aceca533d23 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError class BlueprintException(HomeAssistantError): """Base exception for blueprint errors.""" - def __init__(self, domain: str, msg: str) -> None: + def __init__(self, domain: str | None, msg: str) -> None: """Initialize a blueprint exception.""" super().__init__(msg) self.domain = domain @@ -22,7 +22,9 @@ class BlueprintException(HomeAssistantError): class BlueprintWithNameException(BlueprintException): """Base exception for blueprint errors.""" - def __init__(self, domain: str, blueprint_name: str, msg: str) -> None: + def __init__( + self, domain: str | None, blueprint_name: str | None, msg: str + ) -> None: """Initialize blueprint exception.""" super().__init__(domain, msg) self.blueprint_name = blueprint_name @@ -41,8 +43,8 @@ class InvalidBlueprint(BlueprintWithNameException): def __init__( self, - domain: str, - blueprint_name: str, + domain: str | None, + blueprint_name: str | None, blueprint_data: Any, msg_or_exc: vol.Invalid, ) -> None: diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index de39741d8ed..f8b37a97c31 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -91,12 +91,12 @@ def _get_community_post_import_url(url: str) -> str: def _extract_blueprint_from_community_topic( url: str, topic: dict, -) -> ImportedBlueprint | None: +) -> ImportedBlueprint: """Extract a blueprint from a community post JSON. Async friendly. """ - block_content = None + block_content: str blueprint = None post = topic["post_stream"]["posts"][0] @@ -118,6 +118,7 @@ def _extract_blueprint_from_community_topic( if not is_blueprint_config(data): continue + assert isinstance(data, dict) blueprint = Blueprint(data) break @@ -134,7 +135,7 @@ def _extract_blueprint_from_community_topic( async def fetch_blueprint_from_community_post( hass: HomeAssistant, url: str -) -> ImportedBlueprint | None: +) -> ImportedBlueprint: """Get blueprints from a community post url. Method can raise aiohttp client exceptions, vol.Invalid. @@ -160,6 +161,7 @@ async def fetch_blueprint_from_github_url( resp = await session.get(import_url, raise_for_status=True) raw_yaml = await resp.text() data = yaml.parse_yaml(raw_yaml) + assert isinstance(data, dict) blueprint = Blueprint(data) parsed_import_url = yarl.URL(import_url) @@ -189,7 +191,7 @@ async def fetch_blueprint_from_github_gist_url( blueprint = None filename = None - content = None + content: str for filename, info in gist["files"].items(): if not filename.endswith(".yaml"): @@ -200,6 +202,7 @@ async def fetch_blueprint_from_github_gist_url( if not is_blueprint_config(data): continue + assert isinstance(data, dict) blueprint = Blueprint(data) break diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index a8146764710..0d90c663b4f 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -188,7 +188,7 @@ class DomainBlueprints: self.hass = hass self.domain = domain self.logger = logger - self._blueprints = {} + self._blueprints: dict[str, Blueprint | None] = {} self._load_lock = asyncio.Lock() hass.data.setdefault(DOMAIN, {})[domain] = self @@ -216,19 +216,20 @@ class DomainBlueprints: except HomeAssistantError as err: raise FailedToLoad(self.domain, blueprint_path, err) from err + assert isinstance(blueprint_data, dict) return Blueprint( blueprint_data, expected_domain=self.domain, path=blueprint_path ) - def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException]: + def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException | None]: """Load all the blueprints.""" blueprint_folder = pathlib.Path( self.hass.config.path(BLUEPRINT_FOLDER, self.domain) ) - results = {} + results: dict[str, Blueprint | BlueprintException | None] = {} - for blueprint_path in blueprint_folder.glob("**/*.yaml"): - blueprint_path = str(blueprint_path.relative_to(blueprint_folder)) + for path in blueprint_folder.glob("**/*.yaml"): + blueprint_path = str(path.relative_to(blueprint_folder)) if self._blueprints.get(blueprint_path) is None: try: self._blueprints[blueprint_path] = self._load_blueprint( @@ -245,7 +246,7 @@ class DomainBlueprints: async def async_get_blueprints( self, - ) -> dict[str, Blueprint | BlueprintException]: + ) -> dict[str, Blueprint | BlueprintException | None]: """Get all the blueprints.""" async with self._load_lock: return await self.hass.async_add_executor_job(self._load_blueprints) diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index b8a4c214a2e..0b84d1d08c2 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -24,13 +24,13 @@ def async_setup(hass: HomeAssistant): websocket_api.async_register_command(hass, ws_delete_blueprint) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/list", vol.Required("domain"): cv.string, } ) +@websocket_api.async_response async def ws_list_blueprints(hass, connection, msg): """List available blueprints.""" domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get( @@ -55,13 +55,13 @@ async def ws_list_blueprints(hass, connection, msg): connection.send_result(msg["id"], results) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/import", vol.Required("url"): cv.url, } ) +@websocket_api.async_response async def ws_import_blueprint(hass, connection, msg): """Import a blueprint.""" async with async_timeout.timeout(10): @@ -86,7 +86,6 @@ async def ws_import_blueprint(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/save", @@ -96,6 +95,7 @@ async def ws_import_blueprint(hass, connection, msg): vol.Optional("source_url"): cv.url, } ) +@websocket_api.async_response async def ws_save_blueprint(hass, connection, msg): """Save a blueprint.""" @@ -135,7 +135,6 @@ async def ws_save_blueprint(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/delete", @@ -143,6 +142,7 @@ async def ws_save_blueprint(hass, connection, msg): vol.Required("path"): cv.path, } ) +@websocket_api.async_response async def ws_delete_blueprint(hass, connection, msg): """Delete a blueprint.""" diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py new file mode 100644 index 00000000000..c91563d7729 --- /dev/null +++ b/homeassistant/components/bluetooth/__init__.py @@ -0,0 +1,547 @@ +"""The bluetooth integration.""" +from __future__ import annotations + +import asyncio +from asyncio import Future +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta +from enum import Enum +import logging +from typing import TYPE_CHECKING, Final + +import async_timeout +from bleak import BleakError +from dbus_next import InvalidMessageError + +from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + callback as hass_callback, +) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery_flow +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.loader import async_get_bluetooth +from homeassistant.util.package import is_docker_env + +from . import models +from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN +from .match import ( + ADDRESS, + BluetoothCallbackMatcher, + IntegrationMatcher, + ble_device_matches, +) +from .models import HaBleakScanner, HaBleakScannerWrapper +from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher +from .util import async_get_bluetooth_adapters + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + from homeassistant.helpers.typing import ConfigType + + +_LOGGER = logging.getLogger(__name__) + + +UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 +START_TIMEOUT = 9 + +SOURCE_LOCAL: Final = "local" + + +@dataclass +class BluetoothServiceInfoBleak(BluetoothServiceInfo): + """BluetoothServiceInfo with bleak data. + + Integrations may need BLEDevice and AdvertisementData + to connect to the device without having bleak trigger + another scan to translate the address to the system's + internal details. + """ + + device: BLEDevice + advertisement: AdvertisementData + + @classmethod + def from_advertisement( + cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str + ) -> BluetoothServiceInfoBleak: + """Create a BluetoothServiceInfoBleak from an advertisement.""" + return cls( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=source, + device=device, + advertisement=advertisement_data, + ) + + +class BluetoothScanningMode(Enum): + """The mode of scanning for bluetooth devices.""" + + PASSIVE = "passive" + ACTIVE = "active" + + +SCANNING_MODE_TO_BLEAK = { + BluetoothScanningMode.ACTIVE: "active", + BluetoothScanningMode.PASSIVE: "passive", +} + + +BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") +BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] +ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] + + +@hass_callback +def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: + """Return a HaBleakScannerWrapper. + + This is a wrapper around our BleakScanner singleton that allows + multiple integrations to share the same BleakScanner. + """ + if DOMAIN not in hass.data: + raise RuntimeError("Bluetooth integration not loaded") + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_get_scanner() + + +@hass_callback +def async_discovered_service_info( + hass: HomeAssistant, +) -> list[BluetoothServiceInfoBleak]: + """Return the discovered devices list.""" + if DOMAIN not in hass.data: + return [] + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_discovered_service_info() + + +@hass_callback +def async_ble_device_from_address( + hass: HomeAssistant, + address: str, +) -> BLEDevice | None: + """Return BLEDevice for an address if its present.""" + if DOMAIN not in hass.data: + return None + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_ble_device_from_address(address) + + +@hass_callback +def async_address_present( + hass: HomeAssistant, + address: str, +) -> bool: + """Check if an address is present in the bluetooth device list.""" + if DOMAIN not in hass.data: + return False + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_address_present(address) + + +@hass_callback +def async_register_callback( + hass: HomeAssistant, + callback: BluetoothCallback, + match_dict: BluetoothCallbackMatcher | None, + mode: BluetoothScanningMode, +) -> Callable[[], None]: + """Register to receive a callback on bluetooth change. + + mode is currently not used as we only support active scanning. + Passive scanning will be available in the future. The flag + is required to be present to avoid a future breaking change + when we support passive scanning. + + Returns a callback that can be used to cancel the registration. + """ + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_register_callback(callback, match_dict) + + +async def async_process_advertisements( + hass: HomeAssistant, + callback: ProcessAdvertisementCallback, + match_dict: BluetoothCallbackMatcher, + mode: BluetoothScanningMode, + timeout: int, +) -> BluetoothServiceInfoBleak: + """Process advertisements until callback returns true or timeout expires.""" + done: Future[BluetoothServiceInfoBleak] = Future() + + @hass_callback + def _async_discovered_device( + service_info: BluetoothServiceInfoBleak, change: BluetoothChange + ) -> None: + if callback(service_info): + done.set_result(service_info) + + unload = async_register_callback(hass, _async_discovered_device, match_dict, mode) + + try: + async with async_timeout.timeout(timeout): + return await done + finally: + unload() + + +@hass_callback +def async_track_unavailable( + hass: HomeAssistant, + callback: Callable[[str], None], + address: str, +) -> Callable[[], None]: + """Register to receive a callback when an address is unavailable. + + Returns a callback that can be used to cancel the registration. + """ + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_track_unavailable(callback, address) + + +async def _async_has_bluetooth_adapter() -> bool: + """Return if the device has a bluetooth adapter.""" + return bool(await async_get_bluetooth_adapters()) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the bluetooth integration.""" + integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) + manager = BluetoothManager(hass, integration_matcher) + manager.async_setup() + hass.data[DOMAIN] = manager + # The config entry is responsible for starting the manager + # if its enabled + + if hass.config_entries.async_entries(DOMAIN): + return True + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} + ) + ) + elif await _async_has_bluetooth_adapter(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + ) + return True + + +async def async_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Set up the bluetooth integration from a config entry.""" + manager: BluetoothManager = hass.data[DOMAIN] + await manager.async_start( + BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) + ) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + return True + + +async def _async_update_listener( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> None: + """Handle options update.""" + manager: BluetoothManager = hass.data[DOMAIN] + manager.async_start_reload() + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Unload a config entry.""" + manager: BluetoothManager = hass.data[DOMAIN] + await manager.async_stop() + return True + + +class BluetoothManager: + """Manage Bluetooth.""" + + def __init__( + self, + hass: HomeAssistant, + integration_matcher: IntegrationMatcher, + ) -> None: + """Init bluetooth discovery.""" + self.hass = hass + self._integration_matcher = integration_matcher + self.scanner: HaBleakScanner | None = None + self._cancel_device_detected: CALLBACK_TYPE | None = None + self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None + self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} + self._callbacks: list[ + tuple[BluetoothCallback, BluetoothCallbackMatcher | None] + ] = [] + self._reloading = False + + @hass_callback + def async_setup(self) -> None: + """Set up the bluetooth manager.""" + models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() + + @hass_callback + def async_get_scanner(self) -> HaBleakScannerWrapper: + """Get the scanner.""" + return HaBleakScannerWrapper() + + @hass_callback + def async_start_reload(self) -> None: + """Start reloading.""" + self._reloading = True + + async def async_start( + self, scanning_mode: BluetoothScanningMode, adapter: str | None + ) -> None: + """Set up BT Discovery.""" + assert self.scanner is not None + if self._reloading: + # On reload, we need to reset the scanner instance + # since the devices in its history may not be reachable + # anymore. + self.scanner.async_reset() + self._integration_matcher.async_clear_history() + self._reloading = False + scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} + if adapter and adapter not in DEFAULT_ADAPTERS: + scanner_kwargs["adapter"] = adapter + _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) + try: + self.scanner.async_setup(**scanner_kwargs) + except (FileNotFoundError, BleakError) as ex: + raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex + install_multiple_bleak_catcher() + # We have to start it right away as some integrations might + # need it straight away. + _LOGGER.debug("Starting bluetooth scanner") + self.scanner.register_detection_callback(self.scanner.async_callback_dispatcher) + self._cancel_device_detected = self.scanner.async_register_callback( + self._device_detected, {} + ) + try: + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + self._cancel_device_detected() + _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) + raise ConfigEntryNotReady( + f"Invalid DBus message received: {ex}; try restarting `dbus`" + ) from ex + except BrokenPipeError as ex: + self._cancel_device_detected() + _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + self._cancel_device_detected() + _LOGGER.debug( + "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ConfigEntryNotReady( + f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" + ) from ex + except asyncio.TimeoutError as ex: + self._cancel_device_detected() + raise ConfigEntryNotReady( + f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex + except BleakError as ex: + self._cancel_device_detected() + _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) + raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex + self.async_setup_unavailable_tracking() + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + + @hass_callback + def async_setup_unavailable_tracking(self) -> None: + """Set up the unavailable tracking.""" + + @hass_callback + def _async_check_unavailable(now: datetime) -> None: + """Watch for unavailable devices.""" + scanner = self.scanner + assert scanner is not None + history = set(scanner.history) + active = {device.address for device in scanner.discovered_devices} + disappeared = history.difference(active) + for address in disappeared: + del scanner.history[address] + if not (callbacks := self._unavailable_callbacks.get(address)): + continue + for callback in callbacks: + try: + callback(address) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in unavailable callback") + + self._cancel_unavailable_tracking = async_track_time_interval( + self.hass, + _async_check_unavailable, + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS), + ) + + @hass_callback + def _device_detected( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + matched_domains = self._integration_matcher.match_domains( + device, advertisement_data + ) + _LOGGER.debug( + "Device detected: %s with advertisement_data: %s matched domains: %s", + device.address, + advertisement_data, + matched_domains, + ) + + if not matched_domains and not self._callbacks: + return + + service_info: BluetoothServiceInfoBleak | None = None + for callback, matcher in self._callbacks: + if matcher is None or ble_device_matches( + matcher, device, advertisement_data + ): + if service_info is None: + service_info = BluetoothServiceInfoBleak.from_advertisement( + device, advertisement_data, SOURCE_LOCAL + ) + try: + callback(service_info, BluetoothChange.ADVERTISEMENT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + + if not matched_domains: + return + if service_info is None: + service_info = BluetoothServiceInfoBleak.from_advertisement( + device, advertisement_data, SOURCE_LOCAL + ) + for domain in matched_domains: + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) + + @hass_callback + def async_track_unavailable( + self, callback: Callable[[str], None], address: str + ) -> Callable[[], None]: + """Register a callback.""" + self._unavailable_callbacks.setdefault(address, []).append(callback) + + @hass_callback + def _async_remove_callback() -> None: + self._unavailable_callbacks[address].remove(callback) + if not self._unavailable_callbacks[address]: + del self._unavailable_callbacks[address] + + return _async_remove_callback + + @hass_callback + def async_register_callback( + self, + callback: BluetoothCallback, + matcher: BluetoothCallbackMatcher | None = None, + ) -> Callable[[], None]: + """Register a callback.""" + callback_entry = (callback, matcher) + self._callbacks.append(callback_entry) + + @hass_callback + def _async_remove_callback() -> None: + self._callbacks.remove(callback_entry) + + # If we have history for the subscriber, we can trigger the callback + # immediately with the last packet so the subscriber can see the + # device. + if ( + matcher + and (address := matcher.get(ADDRESS)) + and self.scanner + and (device_adv_data := self.scanner.history.get(address)) + ): + try: + callback( + BluetoothServiceInfoBleak.from_advertisement( + *device_adv_data, SOURCE_LOCAL + ), + BluetoothChange.ADVERTISEMENT, + ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + + return _async_remove_callback + + @hass_callback + def async_ble_device_from_address(self, address: str) -> BLEDevice | None: + """Return the BLEDevice if present.""" + if self.scanner and (ble_adv := self.scanner.history.get(address)): + return ble_adv[0] + return None + + @hass_callback + def async_address_present(self, address: str) -> bool: + """Return if the address is present.""" + return bool(self.scanner and address in self.scanner.history) + + @hass_callback + def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: + """Return if the address is present.""" + assert self.scanner is not None + return [ + BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) + for device_adv in self.scanner.history.values() + ] + + async def async_stop(self, event: Event | None = None) -> None: + """Stop bluetooth discovery.""" + if self._cancel_device_detected: + self._cancel_device_detected() + self._cancel_device_detected = None + if self._cancel_unavailable_tracking: + self._cancel_unavailable_tracking() + self._cancel_unavailable_tracking = None + if self.scanner: + try: + await self.scanner.stop() # type: ignore[no-untyped-call] + except BleakError as ex: + # This is not fatal, and they may want to reload + # the config entry to restart the scanner if they + # change the bluetooth dongle. + _LOGGER.error("Error stopping scanner: %s", ex) + uninstall_multiple_bleak_catcher() diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py new file mode 100644 index 00000000000..1a0be8706bf --- /dev/null +++ b/homeassistant/components/bluetooth/config_flow.py @@ -0,0 +1,80 @@ +"""Config flow to configure the Bluetooth integration.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import voluptuous as vol + +from homeassistant.components import onboarding +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.core import callback + +from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN +from .util import async_get_bluetooth_adapters + +if TYPE_CHECKING: + from homeassistant.data_entry_flow import FlowResult + + +class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Bluetooth.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + return await self.async_step_enable_bluetooth() + + async def async_step_enable_bluetooth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user or import.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + return self.async_create_entry(title=DEFAULT_NAME, data={}) + + return self.async_show_form(step_id="enable_bluetooth") + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_enable_bluetooth(user_input) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(OptionsFlow): + """Handle the option flow for bluetooth.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + if not (adapters := await async_get_bluetooth_adapters()): + return self.async_abort(reason="no_adapters") + + data_schema = vol.Schema( + { + vol.Required( + CONF_ADAPTER, + default=self.config_entry.options.get(CONF_ADAPTER, adapters[0]), + ): vol.In(adapters), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py new file mode 100644 index 00000000000..f3f00f581ee --- /dev/null +++ b/homeassistant/components/bluetooth/const.py @@ -0,0 +1,11 @@ +"""Constants for the Bluetooth integration.""" + +DOMAIN = "bluetooth" +DEFAULT_NAME = "Bluetooth" + +CONF_ADAPTER = "adapter" + +MACOS_DEFAULT_BLUETOOTH_ADAPTER = "CoreBluetooth" +UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" + +DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json new file mode 100644 index 00000000000..40e63ec7180 --- /dev/null +++ b/homeassistant/components/bluetooth/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "bluetooth", + "name": "Bluetooth", + "documentation": "https://www.home-assistant.io/integrations/bluetooth", + "dependencies": ["websocket_api"], + "quality_scale": "internal", + "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.3"], + "codeowners": ["@bdraco"], + "config_flow": true, + "iot_class": "local_push" +} diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py new file mode 100644 index 00000000000..2cd4f62ae5e --- /dev/null +++ b/homeassistant/components/bluetooth/match.py @@ -0,0 +1,147 @@ +"""The bluetooth integration matchers.""" +from __future__ import annotations + +from dataclasses import dataclass +import fnmatch +from typing import TYPE_CHECKING, Final, TypedDict + +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional + +if TYPE_CHECKING: + from collections.abc import Mapping + + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + +MAX_REMEMBER_ADDRESSES: Final = 2048 + + +ADDRESS: Final = "address" +LOCAL_NAME: Final = "local_name" +SERVICE_UUID: Final = "service_uuid" +SERVICE_DATA_UUID: Final = "service_data_uuid" +MANUFACTURER_ID: Final = "manufacturer_id" +MANUFACTURER_DATA_START: Final = "manufacturer_data_start" + + +class BluetoothCallbackMatcherOptional(TypedDict, total=False): + """Matcher for the bluetooth integration for callback optional fields.""" + + address: str + + +class BluetoothCallbackMatcher( + BluetoothMatcherOptional, + BluetoothCallbackMatcherOptional, +): + """Callback matcher for the bluetooth integration.""" + + +@dataclass(frozen=False) +class IntegrationMatchHistory: + """Track which fields have been seen.""" + + manufacturer_data: bool + service_data: bool + service_uuids: bool + + +def seen_all_fields( + previous_match: IntegrationMatchHistory, adv_data: AdvertisementData +) -> bool: + """Return if we have seen all fields.""" + if not previous_match.manufacturer_data and adv_data.manufacturer_data: + return False + if not previous_match.service_data and adv_data.service_data: + return False + if not previous_match.service_uuids and adv_data.service_uuids: + return False + return True + + +class IntegrationMatcher: + """Integration matcher for the bluetooth integration.""" + + def __init__(self, integration_matchers: list[BluetoothMatcher]) -> None: + """Initialize the matcher.""" + self._integration_matchers = integration_matchers + # Some devices use a random address so we need to use + # an LRU to avoid memory issues. + self._matched: Mapping[str, IntegrationMatchHistory] = LRU( + MAX_REMEMBER_ADDRESSES + ) + + def async_clear_history(self) -> None: + """Clear the history.""" + self._matched = {} + + def match_domains(self, device: BLEDevice, adv_data: AdvertisementData) -> set[str]: + """Return the domains that are matched.""" + matched_domains: set[str] = set() + if (previous_match := self._matched.get(device.address)) and seen_all_fields( + previous_match, adv_data + ): + # We have seen all fields so we can skip the rest of the matchers + return matched_domains + matched_domains = { + matcher["domain"] + for matcher in self._integration_matchers + if ble_device_matches(matcher, device, adv_data) + } + if not matched_domains: + return matched_domains + if previous_match: + previous_match.manufacturer_data |= bool(adv_data.manufacturer_data) + previous_match.service_data |= bool(adv_data.service_data) + previous_match.service_uuids |= bool(adv_data.service_uuids) + else: + self._matched[device.address] = IntegrationMatchHistory( # type: ignore[index] + manufacturer_data=bool(adv_data.manufacturer_data), + service_data=bool(adv_data.service_data), + service_uuids=bool(adv_data.service_uuids), + ) + return matched_domains + + +def ble_device_matches( + matcher: BluetoothCallbackMatcher | BluetoothMatcher, + device: BLEDevice, + adv_data: AdvertisementData, +) -> bool: + """Check if a ble device and advertisement_data matches the matcher.""" + if (address := matcher.get(ADDRESS)) is not None and device.address != address: + return False + + if (local_name := matcher.get(LOCAL_NAME)) is not None and not fnmatch.fnmatch( + adv_data.local_name or device.name or device.address, + local_name, + ): + return False + + if ( + service_uuid := matcher.get(SERVICE_UUID) + ) is not None and service_uuid not in adv_data.service_uuids: + return False + + if ( + service_data_uuid := matcher.get(SERVICE_DATA_UUID) + ) is not None and service_data_uuid not in adv_data.service_data: + return False + + if ( + manfacturer_id := matcher.get(MANUFACTURER_ID) + ) is not None and manfacturer_id not in adv_data.manufacturer_data: + return False + + if (manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START)) is not None: + manufacturer_data_start_bytes = bytearray(manufacturer_data_start) + if not any( + manufacturer_data.startswith(manufacturer_data_start_bytes) + for manufacturer_data in adv_data.manufacturer_data.values() + ): + return False + + return True diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py new file mode 100644 index 00000000000..51704a2f530 --- /dev/null +++ b/homeassistant/components/bluetooth/models.py @@ -0,0 +1,202 @@ +"""Models for bluetooth.""" +from __future__ import annotations + +import asyncio +import contextlib +import logging +from typing import TYPE_CHECKING, Any, Final + +from bleak import BleakScanner +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BaseBleakScanner, +) + +from homeassistant.core import CALLBACK_TYPE, callback as hass_callback + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + +_LOGGER = logging.getLogger(__name__) + +FILTER_UUIDS: Final = "UUIDs" + +HA_BLEAK_SCANNER: HaBleakScanner | None = None + + +def _dispatch_callback( + callback: AdvertisementDataCallback, + filters: dict[str, set[str]], + device: BLEDevice, + advertisement_data: AdvertisementData, +) -> None: + """Dispatch the callback.""" + if not callback: + # Callback destroyed right before being called, ignore + return # type: ignore[unreachable] + + if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( + advertisement_data.service_uuids + ): + return + + try: + callback(device, advertisement_data) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in callback: %s", callback) + + +class HaBleakScanner(BleakScanner): + """BleakScanner that cannot be stopped.""" + + def __init__( # pylint: disable=super-init-not-called + self, *args: Any, **kwargs: Any + ) -> None: + """Initialize the BleakScanner.""" + self._callbacks: list[ + tuple[AdvertisementDataCallback, dict[str, set[str]]] + ] = [] + self.history: dict[str, tuple[BLEDevice, AdvertisementData]] = {} + # Init called later in async_setup if we are enabling the scanner + # since init has side effects that can throw exceptions + self._setup = False + + @hass_callback + def async_setup(self, *args: Any, **kwargs: Any) -> None: + """Deferred setup of the BleakScanner since __init__ has side effects.""" + if not self._setup: + super().__init__(*args, **kwargs) + self._setup = True + + @hass_callback + def async_reset(self) -> None: + """Reset the scanner so it can be setup again.""" + self.history = {} + self._setup = False + + @hass_callback + def async_register_callback( + self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] + ) -> CALLBACK_TYPE: + """Register a callback.""" + callback_entry = (callback, filters) + self._callbacks.append(callback_entry) + + @hass_callback + def _remove_callback() -> None: + self._callbacks.remove(callback_entry) + + # Replay the history since otherwise we miss devices + # that were already discovered before the callback was registered + # or we are in passive mode + for device, advertisement_data in self.history.values(): + _dispatch_callback(callback, filters, device, advertisement_data) + + return _remove_callback + + def async_callback_dispatcher( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Dispatch the callback. + + Here we get the actual callback from bleak and dispatch + it to all the wrapped HaBleakScannerWrapper classes + """ + self.history[device.address] = (device, advertisement_data) + for callback_filters in self._callbacks: + _dispatch_callback(*callback_filters, device, advertisement_data) + + +class HaBleakScannerWrapper(BaseBleakScanner): + """A wrapper that uses the single instance.""" + + def __init__( + self, + *args: Any, + detection_callback: AdvertisementDataCallback | None = None, + service_uuids: list[str] | None = None, + **kwargs: Any, + ) -> None: + """Initialize the BleakScanner.""" + self._detection_cancel: CALLBACK_TYPE | None = None + self._mapped_filters: dict[str, set[str]] = {} + self._adv_data_callback: AdvertisementDataCallback | None = None + remapped_kwargs = { + "detection_callback": detection_callback, + "service_uuids": service_uuids or [], + **kwargs, + } + self._map_filters(*args, **remapped_kwargs) + super().__init__( + detection_callback=detection_callback, service_uuids=service_uuids or [] + ) + + async def stop(self, *args: Any, **kwargs: Any) -> None: + """Stop scanning for devices.""" + + async def start(self, *args: Any, **kwargs: Any) -> None: + """Start scanning for devices.""" + + def _map_filters(self, *args: Any, **kwargs: Any) -> bool: + """Map the filters.""" + mapped_filters = {} + if filters := kwargs.get("filters"): + if filter_uuids := filters.get(FILTER_UUIDS): + mapped_filters[FILTER_UUIDS] = set(filter_uuids) + else: + _LOGGER.warning("Only %s filters are supported", FILTER_UUIDS) + if service_uuids := kwargs.get("service_uuids"): + mapped_filters[FILTER_UUIDS] = set(service_uuids) + if mapped_filters == self._mapped_filters: + return False + self._mapped_filters = mapped_filters + return True + + def set_scanning_filter(self, *args: Any, **kwargs: Any) -> None: + """Set the filters to use.""" + if self._map_filters(*args, **kwargs): + self._setup_detection_callback() + + def _cancel_callback(self) -> None: + """Cancel callback.""" + if self._detection_cancel: + self._detection_cancel() + self._detection_cancel = None + + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + assert HA_BLEAK_SCANNER is not None + return HA_BLEAK_SCANNER.discovered_devices + + def register_detection_callback( + self, callback: AdvertisementDataCallback | None + ) -> None: + """Register a callback that is called when a device is discovered or has a property changed. + + This method takes the callback and registers it with the long running + scanner. + """ + self._adv_data_callback = callback + self._setup_detection_callback() + + def _setup_detection_callback(self) -> None: + """Set up the detection callback.""" + if self._adv_data_callback is None: + return + self._cancel_callback() + super().register_detection_callback(self._adv_data_callback) + assert HA_BLEAK_SCANNER is not None + assert self._callback is not None + self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( + self._callback, self._mapped_filters + ) + + def __del__(self) -> None: + """Delete the BleakScanner.""" + if self._detection_cancel: + # Nothing to do if event loop is already closed + with contextlib.suppress(RuntimeError): + asyncio.get_running_loop().call_soon_threadsafe(self._detection_cancel) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py new file mode 100644 index 00000000000..5c6b5b79509 --- /dev/null +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -0,0 +1,90 @@ +"""Passive update coordinator for the Bluetooth integration.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .update_coordinator import BasePassiveBluetoothCoordinator + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + import logging + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + + +class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): + """Class to manage passive bluetooth advertisements. + + This coordinator is responsible for dispatching the bluetooth data + and tracking devices. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + address: str, + mode: BluetoothScanningMode, + ) -> None: + """Initialize PassiveBluetoothDataUpdateCoordinator.""" + super().__init__(hass, logger, address, mode) + self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} + + @callback + def async_update_listeners(self) -> None: + """Update all registered listeners.""" + for update_callback, _ in list(self._listeners.values()): + update_callback() + + @callback + def _async_handle_unavailable(self, address: str) -> None: + """Handle the device going unavailable.""" + super()._async_handle_unavailable(address) + self.async_update_listeners() + + @callback + def async_add_listener( + self, update_callback: CALLBACK_TYPE, context: Any = None + ) -> Callable[[], None]: + """Listen for data updates.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.pop(remove_listener) + + self._listeners[remove_listener] = (update_callback, context) + return remove_listener + + def async_contexts(self) -> Generator[Any, None, None]: + """Return all registered contexts.""" + yield from ( + context for _, context in self._listeners.values() if context is not None + ) + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + super()._async_handle_bluetooth_event(service_info, change) + self.async_update_listeners() + + +class PassiveBluetoothCoordinatorEntity(CoordinatorEntity): + """A class for entities using DataUpdateCoordinator.""" + + coordinator: PassiveBluetoothDataUpdateCoordinator + + async def async_update(self) -> None: + """All updates are passive.""" + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.available diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py new file mode 100644 index 00000000000..78966d9b7ab --- /dev/null +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -0,0 +1,340 @@ +"""Passive update processors for the Bluetooth integration.""" +from __future__ import annotations + +import dataclasses +import logging +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription + +from .const import DOMAIN +from .update_coordinator import BasePassiveBluetoothCoordinator + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping + + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothEntityKey: + """Key for a passive bluetooth entity. + + Example: + key: temperature + device_id: outdoor_sensor_1 + """ + + key: str + device_id: str | None + + +_T = TypeVar("_T") + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothDataUpdate(Generic[_T]): + """Generic bluetooth data.""" + + devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) + entity_descriptions: Mapping[ + PassiveBluetoothEntityKey, EntityDescription + ] = dataclasses.field(default_factory=dict) + entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field( + default_factory=dict + ) + entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field( + default_factory=dict + ) + + +class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): + """Passive bluetooth processor coordinator for bluetooth advertisements. + + The coordinator is responsible for dispatching the bluetooth data, + to each processor, and tracking devices. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + address: str, + mode: BluetoothScanningMode, + ) -> None: + """Initialize the coordinator.""" + super().__init__(hass, logger, address, mode) + self._processors: list[PassiveBluetoothDataProcessor] = [] + + @callback + def async_register_processor( + self, processor: PassiveBluetoothDataProcessor + ) -> Callable[[], None]: + """Register a processor that subscribes to updates.""" + processor.coordinator = self + + @callback + def remove_processor() -> None: + """Remove a processor.""" + self._processors.remove(processor) + + self._processors.append(processor) + return remove_processor + + @callback + def _async_handle_unavailable(self, address: str) -> None: + """Handle the device going unavailable.""" + super()._async_handle_unavailable(address) + for processor in self._processors: + processor.async_handle_unavailable() + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + super()._async_handle_bluetooth_event(service_info, change) + if self.hass.is_stopping: + return + for processor in self._processors: + processor.async_handle_bluetooth_event(service_info, change) + + +_PassiveBluetoothDataProcessorT = TypeVar( + "_PassiveBluetoothDataProcessorT", + bound="PassiveBluetoothDataProcessor[Any]", +) + + +class PassiveBluetoothDataProcessor(Generic[_T]): + """Passive bluetooth data processor for bluetooth advertisements. + + The processor is responsible for keeping track of the bluetooth data + and updating subscribers. + + The update_method must return a PassiveBluetoothDataUpdate object. Callers + are responsible for formatting the data returned from their parser into + the appropriate format. + + The processor will call the update_method every time the bluetooth device + receives a new advertisement data from the coordinator with the following signature: + + update_method(service_info: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate + + As the size of each advertisement is limited, the update_method should + return a PassiveBluetoothDataUpdate object that contains only data that + should be updated. The coordinator will then dispatch subscribers based + on the data in the PassiveBluetoothDataUpdate object. The accumulated data + is available in the devices, entity_data, and entity_descriptions attributes. + """ + + coordinator: PassiveBluetoothProcessorCoordinator + + def __init__( + self, + update_method: Callable[ + [BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T] + ], + ) -> None: + """Initialize the coordinator.""" + self.coordinator: PassiveBluetoothProcessorCoordinator + self._listeners: list[ + Callable[[PassiveBluetoothDataUpdate[_T] | None], None] + ] = [] + self._entity_key_listeners: dict[ + PassiveBluetoothEntityKey, + list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], + ] = {} + self.update_method = update_method + self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} + self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} + self.entity_descriptions: dict[ + PassiveBluetoothEntityKey, EntityDescription + ] = {} + self.devices: dict[str | None, DeviceInfo] = {} + self.last_update_success = True + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self.coordinator.available and self.last_update_success + + @callback + def async_handle_unavailable(self) -> None: + """Handle the device going unavailable.""" + self.async_update_listeners(None) + + @callback + def async_add_entities_listener( + self, + entity_class: type[PassiveBluetoothProcessorEntity], + async_add_entites: AddEntitiesCallback, + ) -> Callable[[], None]: + """Add a listener for new entities.""" + created: set[PassiveBluetoothEntityKey] = set() + + @callback + def _async_add_or_update_entities( + data: PassiveBluetoothDataUpdate[_T] | None, + ) -> None: + """Listen for new entities.""" + if data is None: + return + entities: list[PassiveBluetoothProcessorEntity] = [] + for entity_key, description in data.entity_descriptions.items(): + if entity_key not in created: + entities.append(entity_class(self, entity_key, description)) + created.add(entity_key) + if entities: + async_add_entites(entities) + + return self.async_add_listener(_async_add_or_update_entities) + + @callback + def async_add_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + ) -> Callable[[], None]: + """Listen for all updates.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.remove(update_callback) + + self._listeners.append(update_callback) + return remove_listener + + @callback + def async_add_entity_key_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + entity_key: PassiveBluetoothEntityKey, + ) -> Callable[[], None]: + """Listen for updates by device key.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._entity_key_listeners[entity_key].remove(update_callback) + if not self._entity_key_listeners[entity_key]: + del self._entity_key_listeners[entity_key] + + self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) + return remove_listener + + @callback + def async_update_listeners( + self, data: PassiveBluetoothDataUpdate[_T] | None + ) -> None: + """Update all registered listeners.""" + # Dispatch to listeners without a filter key + for update_callback in self._listeners: + update_callback(data) + + # Dispatch to listeners with a filter key + for listeners in self._entity_key_listeners.values(): + for update_callback in listeners: + update_callback(data) + + @callback + def async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + try: + new_data = self.update_method(service_info) + except Exception as err: # pylint: disable=broad-except + self.last_update_success = False + self.coordinator.logger.exception( + "Unexpected error updating %s data: %s", self.coordinator.name, err + ) + return + + if not isinstance(new_data, PassiveBluetoothDataUpdate): + self.last_update_success = False # type: ignore[unreachable] + raise ValueError( + f"The update_method for {self.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" + ) + + if not self.last_update_success: + self.last_update_success = True + self.coordinator.logger.info( + "Processing %s data recovered", self.coordinator.name + ) + + self.devices.update(new_data.devices) + self.entity_descriptions.update(new_data.entity_descriptions) + self.entity_data.update(new_data.entity_data) + self.entity_names.update(new_data.entity_names) + self.async_update_listeners(new_data) + + +class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]): + """A class for entities using PassiveBluetoothDataProcessor.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + processor: _PassiveBluetoothDataProcessorT, + entity_key: PassiveBluetoothEntityKey, + description: EntityDescription, + context: Any = None, + ) -> None: + """Create the entity with a PassiveBluetoothDataProcessor.""" + self.entity_description = description + self.entity_key = entity_key + self.processor = processor + self.processor_context = context + address = processor.coordinator.address + device_id = entity_key.device_id + devices = processor.devices + key = entity_key.key + if device_id in devices: + base_device_info = devices[device_id] + else: + base_device_info = DeviceInfo({}) + if device_id: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}} + ) + self._attr_unique_id = f"{address}-{key}-{device_id}" + else: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, address)}} + ) + self._attr_unique_id = f"{address}-{key}" + if ATTR_NAME not in self._attr_device_info: + self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name + self._attr_name = processor.entity_names.get(entity_key) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.processor.available + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.processor.async_add_entity_key_listener( + self._handle_processor_update, self.entity_key + ) + ) + + @callback + def _handle_processor_update( + self, new_data: PassiveBluetoothDataUpdate | None + ) -> None: + """Handle updated data from the processor.""" + self.async_write_ha_state() diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json new file mode 100644 index 00000000000..beff2fd8312 --- /dev/null +++ b/homeassistant/components/bluetooth/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, + "user": { + "description": "Choose a device to setup", + "data": { + "address": "Device" + } + }, + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "no_adapters": "No Bluetooth adapters found" + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "The Bluetooth Adapter to use for scanning" + } + } + } + } +} diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json new file mode 100644 index 00000000000..082803a48dc --- /dev/null +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "no_adapters": "No s'ha trobat cap adaptador Bluetooth" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "enable_bluetooth": { + "description": "Vols configurar Bluetooth?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Adaptador Bluetooth a utilitzar per escanejar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json new file mode 100644 index 00000000000..6e65b985478 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "no_adapters": "Keine Bluetooth-Adapter gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "enable_bluetooth": { + "description": "M\u00f6chtest du Bluetooth einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Der zum Scannen zu verwendende Bluetooth-Adapter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json new file mode 100644 index 00000000000..5a0aee96322 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/el.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "no_adapters": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03b5\u03af\u03c2 Bluetooth" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "enable_bluetooth": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Bluetooth;" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u039f \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2 Bluetooth \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json new file mode 100644 index 00000000000..4b53822b771 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "no_adapters": "No Bluetooth adapters found" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "The Bluetooth Adapter to use for scanning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json new file mode 100644 index 00000000000..da1dbdb1a5f --- /dev/null +++ b/homeassistant/components/bluetooth/translations/et.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "no_adapters": "Bluetoothi adaptereid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name} ?" + }, + "enable_bluetooth": { + "description": "Kas soovid Bluetoothi seadistada?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Sk\u00e4nnimiseks kasutatav Bluetoothi adapter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/fr.json b/homeassistant/components/bluetooth/translations/fr.json new file mode 100644 index 00000000000..80a0ac6ea3f --- /dev/null +++ b/homeassistant/components/bluetooth/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "enable_bluetooth": { + "description": "Voulez-vous configurer le Bluetooth\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json new file mode 100644 index 00000000000..5cdc199476c --- /dev/null +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "enable_bluetooth": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a Bluetooth-ot?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json new file mode 100644 index 00000000000..5fe99b68c2f --- /dev/null +++ b/homeassistant/components/bluetooth/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "enable_bluetooth": { + "description": "Ingin menyiapkan Bluetooth?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json new file mode 100644 index 00000000000..86809b41a7d --- /dev/null +++ b/homeassistant/components/bluetooth/translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "no_adapters": "Nessun adattatore Bluetooth trovato" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "enable_bluetooth": { + "description": "Vuoi configurare il Bluetooth?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "L'adattatore Bluetooth da utilizzare per la scansione" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json new file mode 100644 index 00000000000..6257d1c67d4 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "enable_bluetooth": { + "description": "Bluetooth\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json new file mode 100644 index 00000000000..f7e6fe060af --- /dev/null +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "no_adapters": "Nie znaleziono adapter\u00f3w Bluetooth" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "enable_bluetooth": { + "description": "Czy chcesz skonfigurowa\u0107 Bluetooth?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Adapter Bluetooth u\u017cywany do skanowania" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json new file mode 100644 index 00000000000..0a5cf354495 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "no_adapters": "Nenhum adaptador Bluetooth encontrado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "enable_bluetooth": { + "description": "Deseja configurar o Bluetooth?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "O adaptador Bluetooth a ser usado para escaneamento" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json new file mode 100644 index 00000000000..802470d7c29 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "no_adapters": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440\u044b Bluetooth \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "enable_bluetooth": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Bluetooth?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json new file mode 100644 index 00000000000..34ab75775ab --- /dev/null +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_adapters": "\u627e\u4e0d\u5230\u4efb\u4f55\u85cd\u82bd\u50b3\u8f38\u5668" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "enable_bluetooth": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u82bd\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u82bd\u50b3\u8f38\u5668" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py new file mode 100644 index 00000000000..d0f38ce32c6 --- /dev/null +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -0,0 +1,98 @@ +"""Update coordinator for the Bluetooth integration.""" +from __future__ import annotations + +import logging +import time + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback + +from . import ( + BluetoothCallbackMatcher, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_register_callback, + async_track_unavailable, +) + + +class BasePassiveBluetoothCoordinator: + """Base class for passive bluetooth coordinator for bluetooth advertisements. + + The coordinator is responsible for tracking devices. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + address: str, + mode: BluetoothScanningMode, + ) -> None: + """Initialize the coordinator.""" + self.hass = hass + self.logger = logger + self.name: str | None = None + self.address = address + self._cancel_track_unavailable: CALLBACK_TYPE | None = None + self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None + self._present = False + self.mode = mode + self.last_seen = 0.0 + + @callback + def async_start(self) -> CALLBACK_TYPE: + """Start the data updater.""" + self._async_start() + + @callback + def _async_cancel() -> None: + self._async_stop() + + return _async_cancel + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._present + + @callback + def _async_start(self) -> None: + """Start the callbacks.""" + self._cancel_bluetooth_advertisements = async_register_callback( + self.hass, + self._async_handle_bluetooth_event, + BluetoothCallbackMatcher(address=self.address), + self.mode, + ) + self._cancel_track_unavailable = async_track_unavailable( + self.hass, + self._async_handle_unavailable, + self.address, + ) + + @callback + def _async_stop(self) -> None: + """Stop the callbacks.""" + if self._cancel_bluetooth_advertisements is not None: + self._cancel_bluetooth_advertisements() + self._cancel_bluetooth_advertisements = None + if self._cancel_track_unavailable is not None: + self._cancel_track_unavailable() + self._cancel_track_unavailable = None + + @callback + def _async_handle_unavailable(self, address: str) -> None: + """Handle the device going unavailable.""" + self._present = False + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.last_seen = time.monotonic() + self.name = service_info.name + self._present = True diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py new file mode 100644 index 00000000000..b3a6783cf30 --- /dev/null +++ b/homeassistant/components/bluetooth/usage.py @@ -0,0 +1,19 @@ +"""bluetooth usage utility to handle multiple instances.""" + +from __future__ import annotations + +import bleak + +from .models import HaBleakScannerWrapper + +ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner + + +def install_multiple_bleak_catcher() -> None: + """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" + bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] + + +def uninstall_multiple_bleak_catcher() -> None: + """Unwrap the bleak classes.""" + bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py new file mode 100644 index 00000000000..68920050748 --- /dev/null +++ b/homeassistant/components/bluetooth/util.py @@ -0,0 +1,27 @@ +"""The bluetooth integration utilities.""" +from __future__ import annotations + +import platform + +from .const import MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER + + +async def async_get_bluetooth_adapters() -> list[str]: + """Return a list of bluetooth adapters.""" + if platform.system() == "Windows": # We don't have a good way to detect on windows + return [] + if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware + return [MACOS_DEFAULT_BLUETOOTH_ADAPTER] + from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel + get_bluetooth_adapters, + ) + + adapters = await get_bluetooth_adapters() + if ( + UNIX_DEFAULT_BLUETOOTH_ADAPTER in adapters + and adapters[0] != UNIX_DEFAULT_BLUETOOTH_ADAPTER + ): + # The default adapter always needs to be the first in the list + # because that is how bleak works. + adapters.insert(0, adapters.pop(adapters.index(UNIX_DEFAULT_BLUETOOTH_ADAPTER))) + return adapters diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index b44a180988d..01b3d48205a 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -2,19 +2,19 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging from uuid import UUID -import pygatt +from bleak import BleakClient, BleakError import voluptuous as vol +from homeassistant.components import bluetooth from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, ) from homeassistant.components.device_tracker.const import ( - CONF_SCAN_INTERVAL, CONF_TRACK_NEW, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE, @@ -23,10 +23,10 @@ from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, async_load_config, ) -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -53,33 +53,31 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def setup_scanner( # noqa: C901 +async def async_setup_scanner( # noqa: C901 hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + async_see: Callable[..., Awaitable[None]], discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Bluetooth LE Scanner.""" new_devices: dict[str, dict] = {} - hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) - - def handle_stop(event): - """Try to shut down the bluetooth child process nicely.""" - # These should never be unset at the point this runs, but just for - # safety's sake, use `get`. - adapter = hass.data.get(DATA_BLE, {}).get(DATA_BLE_ADAPTER) - if adapter is not None: - adapter.kill() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) if config[CONF_TRACK_BATTERY]: battery_track_interval = config[CONF_TRACK_BATTERY_INTERVAL] else: battery_track_interval = timedelta(0) - def see_device(address, name, new_device=False, battery=None): + yaml_path = hass.config.path(YAML_DEVICES) + devs_to_track: set[str] = set() + devs_no_track: set[str] = set() + devs_track_battery = {} + interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + # if track new devices is true discover new devices + # on every scan. + track_new = config.get(CONF_TRACK_NEW) + + async def async_see_device(address, name, new_device=False, battery=None): """Mark a device as seen.""" if name is not None: name = name.strip("\x00") @@ -95,7 +93,7 @@ def setup_scanner( # noqa: C901 if new_devices[address]["seen"] < MIN_SEEN_NEW: return _LOGGER.debug("Adding %s to tracked devices", address) - devs_to_track.append(address) + devs_to_track.add(address) if battery_track_interval > timedelta(0): devs_track_battery[address] = dt_util.as_utc( datetime.fromtimestamp(0) @@ -105,109 +103,113 @@ def setup_scanner( # noqa: C901 new_devices[address] = {"seen": 1, "name": name} return - see( + await async_see( mac=BLE_PREFIX + address, host_name=name, source_type=SOURCE_TYPE_BLUETOOTH_LE, battery=battery, ) - def discover_ble_devices(): - """Discover Bluetooth LE devices.""" - _LOGGER.debug("Discovering Bluetooth LE devices") - try: - adapter = pygatt.GATTToolBackend() - hass.data[DATA_BLE][DATA_BLE_ADAPTER] = adapter - devs = adapter.scan() - - devices = {x["address"]: x["name"] for x in devs} - _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) - except (RuntimeError, pygatt.exceptions.BLEError) as error: - _LOGGER.error("Error during Bluetooth LE scan: %s", error) - return {} - return devices - - yaml_path = hass.config.path(YAML_DEVICES) - devs_to_track = [] - devs_donot_track = [] - devs_track_battery = {} - # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in asyncio.run_coroutine_threadsafe( - async_load_config(yaml_path, hass, timedelta(0)), hass.loop - ).result(): + for device in await async_load_config(yaml_path, hass, timedelta(0)): # check if device is a valid bluetooth device if device.mac and device.mac[:4].upper() == BLE_PREFIX: address = device.mac[4:] if device.track: _LOGGER.debug("Adding %s to BLE tracker", device.mac) - devs_to_track.append(address) + devs_to_track.add(address) if battery_track_interval > timedelta(0): devs_track_battery[address] = dt_util.as_utc( datetime.fromtimestamp(0) ) else: _LOGGER.debug("Adding %s to BLE do not track", device.mac) - devs_donot_track.append(address) - - # if track new devices is true discover new devices - # on every scan. - track_new = config.get(CONF_TRACK_NEW) + devs_no_track.add(address) if not devs_to_track and not track_new: _LOGGER.warning("No Bluetooth LE devices to track!") return False - interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - - def update_ble(now): + async def _async_see_update_ble_battery( + mac: str, + now: datetime, + service_info: bluetooth.BluetoothServiceInfoBleak, + ) -> None: """Lookup Bluetooth LE devices and update status.""" - devs = discover_ble_devices() - if devs_track_battery: - adapter = hass.data[DATA_BLE][DATA_BLE_ADAPTER] - for mac in devs_to_track: - if mac not in devs: - continue + battery = None + try: + async with BleakClient(service_info.device) as client: + bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) + battery = ord(bat_char) + except asyncio.TimeoutError: + _LOGGER.warning( + "Timeout when trying to get battery status for %s", service_info.name + ) + # Bleak currently has a few places where checking dbus attributes + # can raise when there is another error. We need to trap AttributeError + # until bleak releases v0.15+ which resolves these. + except (AttributeError, BleakError) as err: + _LOGGER.debug("Could not read battery status: %s", err) + # If the device does not offer battery information, there is no point in asking again later on. + # Remove the device from the battery-tracked devices, so that their battery is not wasted + # trying to get an unavailable information. + del devs_track_battery[mac] + if battery: + await async_see_device(mac, service_info.name, battery=battery) - if devs[mac] is None: - devs[mac] = mac - - battery = None + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a ble callback.""" + mac = service_info.address + if mac in devs_to_track: + now = dt_util.utcnow() + hass.async_create_task(async_see_device(mac, service_info.name)) if ( mac in devs_track_battery and now > devs_track_battery[mac] + battery_track_interval ): - handle = None - try: - adapter.start(reset_on_start=False) - _LOGGER.debug("Reading battery for Bluetooth LE device %s", mac) - bt_device = adapter.connect(mac) - # Try to get the handle; it will raise a BLEError exception if not available - handle = bt_device.get_handle(BATTERY_CHARACTERISTIC_UUID) - battery = ord(bt_device.char_read(BATTERY_CHARACTERISTIC_UUID)) - devs_track_battery[mac] = now - except pygatt.exceptions.NotificationTimeout: - _LOGGER.warning("Timeout when trying to get battery status") - except pygatt.exceptions.BLEError as err: - _LOGGER.warning("Could not read battery status: %s", err) - if handle is not None: - # If the device does not offer battery information, there is no point in asking again later on. - # Remove the device from the battery-tracked devices, so that their battery is not wasted - # trying to get an unavailable information. - del devs_track_battery[mac] - finally: - adapter.stop() - see_device(mac, devs[mac], battery=battery) + devs_track_battery[mac] = now + asyncio.create_task( + _async_see_update_ble_battery(mac, now, service_info) + ) if track_new: - for address in devs: - if address not in devs_to_track and address not in devs_donot_track: - _LOGGER.info("Discovered Bluetooth LE device %s", address) - see_device(address, devs[address], new_device=True) + if mac not in devs_to_track and mac not in devs_no_track: + _LOGGER.info("Discovered Bluetooth LE device %s", mac) + hass.async_create_task( + async_see_device(mac, service_info.name, new_device=True) + ) - track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval) + @callback + def _async_refresh_ble(now: datetime) -> None: + """Refresh BLE devices from the discovered service info.""" + # Make sure devices are seen again at the scheduled + # interval so they do not get set to not_home when + # there have been no callbacks because the RSSI or + # other properties have not changed. + for service_info in bluetooth.async_discovered_service_info(hass): + _async_update_ble(service_info, bluetooth.BluetoothChange.ADVERTISEMENT) + + cancels = [ + bluetooth.async_register_callback( + hass, _async_update_ble, None, bluetooth.BluetoothScanningMode.ACTIVE + ), + async_track_time_interval(hass, _async_refresh_ble, interval), + ] + + @callback + def _async_handle_stop(event: Event) -> None: + """Cancel the callback.""" + for cancel in cancels: + cancel() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_handle_stop) + + _async_refresh_ble(dt_util.now()) - update_ble(dt_util.utcnow()) return True diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 7552c024d62..6d1d4ba2d4a 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -2,8 +2,8 @@ "domain": "bluetooth_le_tracker", "name": "Bluetooth LE Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", - "requirements": ["pygatt[GATTTOOL]==4.0.5"], + "dependencies": ["bluetooth"], "codeowners": [], - "iot_class": "local_polling", - "loggers": ["pygatt"] + "iot_class": "local_push", + "loggers": [] } diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 7023dd7481a..959fbe04240 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -127,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms except notify - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] ) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index a19ccc8f715..94d8902fe23 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -39,14 +39,8 @@ def _condition_based_services( def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]: extra_attributes: dict[str, Any] = {} - if vehicle.check_control_messages.has_check_control_messages: - cbs_list = [ - message.description_short - for message in vehicle.check_control_messages.messages - ] - extra_attributes["check_control_messages"] = cbs_list - else: - extra_attributes["check_control_messages"] = "OK" + for message in vehicle.check_control_messages.messages: + extra_attributes.update({message.description_short: message.state.value}) return extra_attributes diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 7dca4db507d..46066d9f55e 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _async_remove_old_device_identifiers(config_entry_id, device_registry, hub) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 9a82309e347..6f57a23b079 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -88,6 +88,13 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = ( mutually_exclusive=Action.TURN_DOWN_LIGHT_ON, argument=None, ), + BondButtonEntityDescription( + key=Action.START_DIMMER, + name="Start Dimmer", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_BRIGHTNESS, + argument=None, + ), BondButtonEntityDescription( key=Action.START_UP_LIGHT_DIMMER, name="Start Up Light Dimmer", diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json index 179ec599d9f..801a3bf8a44 100644 --- a/homeassistant/components/bond/translations/hu.json +++ b/homeassistant/components/bond/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtse, miel\u0151tt folytatn\u00e1", + "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rem friss\u00edtse, miel\u0151tt folytatn\u00e1", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/bond/translations/pt.json b/homeassistant/components/bond/translations/pt.json index 2173d698932..828e7c55baf 100644 --- a/homeassistant/components/bond/translations/pt.json +++ b/homeassistant/components/bond/translations/pt.json @@ -8,6 +8,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index 4d076a784d1..a8b2a389f9f 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=shc_info.version, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def stop_polling(event): """Stop polling service.""" diff --git a/homeassistant/components/bosch_shc/translations/cs.json b/homeassistant/components/bosch_shc/translations/cs.json index 72df4a96818..45e02001105 100644 --- a/homeassistant/components/bosch_shc/translations/cs.json +++ b/homeassistant/components/bosch_shc/translations/cs.json @@ -4,6 +4,7 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json index df5a484cabe..2008965d95d 100644 --- a/homeassistant/components/bosch_shc/translations/hu.json +++ b/homeassistant/components/bosch_shc/translations/hu.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "pairing_failed": "A p\u00e1ros\u00edt\u00e1s nem siker\u00fclt; K\u00e9rj\u00fck, ellen\u0151rizze, hogy a Bosch Smart Home Controller p\u00e1ros\u00edt\u00e1si m\u00f3dban van-e (villog a LED), \u00e9s hogy a jelszava helyes-e.", + "pairing_failed": "A p\u00e1ros\u00edt\u00e1s nem siker\u00fclt; K\u00e9rem, ellen\u0151rizze, hogy a Bosch Smart Home Controller p\u00e1ros\u00edt\u00e1si m\u00f3dban van-e (villog a LED), \u00e9s hogy a jelszava helyes-e.", "session_error": "Munkamenet hiba: Az API nem OK eredm\u00e9nyt ad vissza.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "K\u00e9rj\u00fck, addig nyomja a Bosch Smart Home Controller el\u00fcls\u0151 gombj\u00e1t, am\u00edg a LED villogni nem kezd.\nK\u00e9szen \u00e1ll {model} @ {host} be\u00e1ll\u00edt\u00e1s\u00e1nak folytat\u00e1s\u00e1ra Home Assistant seg\u00edts\u00e9g\u00e9vel?" + "description": "K\u00e9rem, addig nyomja a Bosch Smart Home Controller el\u00fcls\u0151 gombj\u00e1t, am\u00edg a LED villogni nem kezd.\nK\u00e9szen \u00e1ll {model} @ {host} be\u00e1ll\u00edt\u00e1s\u00e1nak folytat\u00e1s\u00e1ra Home Assistant seg\u00edts\u00e9g\u00e9vel?" }, "credentials": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index da4b471c621..6d7883de038 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -22,8 +22,8 @@ } }, "reauth_confirm": { - "description": "Bosch_shc\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Bosch_shc\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/pt.json b/homeassistant/components/bosch_shc/translations/pt.json new file mode 100644 index 00000000000..e229572938d --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 3962e953520..e1d90681d2a 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 01746cbe963..4aa44992cbf 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -12,6 +12,5 @@ CONF_IGNORED_SOURCES: Final = "ignored_sources" BRAVIA_CONFIG_FILE: Final = "bravia.conf" CLIENTID_PREFIX: Final = "HomeAssistant" -DEFAULT_NAME: Final = f"{ATTR_MANUFACTURER} Bravia TV" DOMAIN: Final = "braviatv" NICKNAME: Final = "Home Assistant" diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py new file mode 100644 index 00000000000..ad896ae8c5a --- /dev/null +++ b/homeassistant/components/braviatv/entity.py @@ -0,0 +1,29 @@ +"""A entity class for BraviaTV integration.""" +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import BraviaTVCoordinator +from .const import ATTR_MANUFACTURER, DOMAIN + + +class BraviaTVEntity(CoordinatorEntity[BraviaTVCoordinator]): + """BraviaTV entity class.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: BraviaTVCoordinator, + unique_id: str, + model: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model=model, + name=f"{ATTR_MANUFACTURER} {model}", + ) diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 745325a4c39..5d812788563 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -9,12 +9,10 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BraviaTVCoordinator -from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN +from .const import DOMAIN +from .entity import BraviaTVEntity async def async_setup_entry( @@ -27,19 +25,13 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id assert unique_id is not None - device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - manufacturer=ATTR_MANUFACTURER, - model=config_entry.title, - name=DEFAULT_NAME, - ) async_add_entities( - [BraviaTVMediaPlayer(coordinator, DEFAULT_NAME, unique_id, device_info)] + [BraviaTVMediaPlayer(coordinator, unique_id, config_entry.title)] ) -class BraviaTVMediaPlayer(CoordinatorEntity[BraviaTVCoordinator], MediaPlayerEntity): +class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Representation of a Bravia TV Media Player.""" _attr_device_class = MediaPlayerDeviceClass.TV @@ -57,21 +49,6 @@ class BraviaTVMediaPlayer(CoordinatorEntity[BraviaTVCoordinator], MediaPlayerEnt | MediaPlayerEntityFeature.STOP ) - def __init__( - self, - coordinator: BraviaTVCoordinator, - name: str, - unique_id: str, - device_info: DeviceInfo, - ) -> None: - """Initialize the entity.""" - - self._attr_device_info = device_info - self._attr_name = name - self._attr_unique_id = unique_id - - super().__init__(coordinator) - @property def state(self) -> str | None: """Return the state of the device.""" diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 016f8363b09..f45b2d74004 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -7,12 +7,10 @@ from typing import Any from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BraviaTVCoordinator -from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN +from .const import DOMAIN +from .entity import BraviaTVEntity async def async_setup_entry( @@ -25,36 +23,13 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id assert unique_id is not None - device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - manufacturer=ATTR_MANUFACTURER, - model=config_entry.title, - name=DEFAULT_NAME, - ) - async_add_entities( - [BraviaTVRemote(coordinator, DEFAULT_NAME, unique_id, device_info)] - ) + async_add_entities([BraviaTVRemote(coordinator, unique_id, config_entry.title)]) -class BraviaTVRemote(CoordinatorEntity[BraviaTVCoordinator], RemoteEntity): +class BraviaTVRemote(BraviaTVEntity, RemoteEntity): """Representation of a Bravia TV Remote.""" - def __init__( - self, - coordinator: BraviaTVCoordinator, - name: str, - unique_id: str, - device_info: DeviceInfo, - ) -> None: - """Initialize the entity.""" - - self._attr_device_info = device_info - self._attr_name = name - self._attr_unique_id = unique_id - - super().__init__(coordinator) - @property def is_on(self) -> bool: """Return true if device is on.""" diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index ff00828c0f3..035de7bc060 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -14,7 +14,7 @@ "data": { "pin": "PIN-Code" }, - "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe daf\u00fcr zu: Einstellungen -> Netzwerk -> Remote - Ger\u00e4teeinstellungen -> Registrierung des entfernten Ger\u00e4ts aufheben.", + "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe daf\u00fcr zu: Einstellungen \u2192 Netzwerk \u2192 Remote - Ger\u00e4teeinstellungen \u2192 Registrierung des entfernten Ger\u00e4ts aufheben.", "title": "Autorisiere Sony Bravia TV" }, "user": { diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 860fd5c89ea..263de9e35b0 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -21,7 +21,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30bd\u30cb\u30fc Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u30bd\u30cb\u30fc Bravia TV\u306e\u7d71\u5408\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } }, diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index e113d74d6fc..0838bb5d632 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -4,7 +4,7 @@ "already_configured": "Esta TV j\u00e1 est\u00e1 configurada." }, "error": { - "cannot_connect": "Falha na conex\u00e3o, nome de servidor inv\u00e1lido ou c\u00f3digo PIN.", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", "unsupported_model": "O seu modelo de TV n\u00e3o \u00e9 suportado." }, diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 46582334e2d..c279e7860b6 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -126,7 +126,7 @@ class BroadlinkDevice: self.reset_jobs.append(config.add_update_listener(self.async_update)) # Forward entry setup to related domains. - self.hass.config_entries.async_setup_platforms( + await self.hass.config_entries.async_forward_entry_setups( config, get_domains(self.api.type) ) diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 96e2ad069ce..7c3d3003ea6 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator hass.data[DOMAIN][SNMP] = snmp_engine - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index a589ea0bd77..b6af96087af 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -111,6 +111,8 @@ async def async_setup_entry( class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): """Define an Brother Printer sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: BrotherDataUpdateCoordinator, @@ -121,7 +123,6 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) self._attrs: dict[str, Any] = {} self._attr_device_info = device_info - self._attr_name = f"{coordinator.data.model} {description.name}" self._attr_unique_id = f"{coordinator.data.serial.lower()}_{description.key}" self.entity_description = description @@ -168,13 +169,13 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_STATUS, icon="mdi:printer", - name=ATTR_STATUS.title(), + name="Status", entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_PAGE_COUNTER, icon="mdi:file-document-outline", - name=ATTR_PAGE_COUNTER.replace("_", " ").title(), + name="Page counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -182,7 +183,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BW_COUNTER, icon="mdi:file-document-outline", - name=ATTR_BW_COUNTER.replace("_", " ").title(), + name="B/W counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -190,7 +191,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_COLOR_COUNTER, icon="mdi:file-document-outline", - name=ATTR_COLOR_COUNTER.replace("_", " ").title(), + name="Color counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -198,7 +199,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_DUPLEX_COUNTER, icon="mdi:file-document-outline", - name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), + name="Duplex unit pages counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -206,7 +207,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -214,7 +215,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BLACK_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Black drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -222,7 +223,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_CYAN_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Cyan drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -230,7 +231,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Magenta drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -238,7 +239,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_YELLOW_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Yellow drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -246,7 +247,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BELT_UNIT_REMAINING_LIFE, icon="mdi:current-ac", - name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), + name="Belt unit remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -254,7 +255,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_FUSER_REMAINING_LIFE, icon="mdi:water-outline", - name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), + name="Fuser remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -262,7 +263,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_LASER_REMAINING_LIFE, icon="mdi:spotlight-beam", - name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), + name="Laser remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -270,7 +271,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_PF_KIT_1_REMAINING_LIFE, icon="mdi:printer-3d", - name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), + name="PF Kit 1 remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -278,7 +279,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_PF_KIT_MP_REMAINING_LIFE, icon="mdi:printer-3d", - name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), + name="PF Kit MP remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -286,7 +287,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BLACK_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), + name="Black toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -294,7 +295,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_CYAN_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), + name="Cyan toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -302,7 +303,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_MAGENTA_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), + name="Magenta toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -310,7 +311,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_YELLOW_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), + name="Yellow toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -318,7 +319,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BLACK_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), + name="Black ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -326,7 +327,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_CYAN_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), + name="Cyan ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -334,7 +335,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_MAGENTA_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), + name="Magenta ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -342,14 +343,14 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_YELLOW_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), + name="Yellow ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_UPTIME, - name=ATTR_UPTIME.title(), + name="Uptime", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 988a96ce08e..f189be63920 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_BAPI: bapi, DATA_COOR: coordinator} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/brunt/translations/hu.json b/homeassistant/components/brunt/translations/hu.json index 3abb5cbf297..bc7be29ba1b 100644 --- a/homeassistant/components/brunt/translations/hu.json +++ b/homeassistant/components/brunt/translations/hu.json @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rem, adja meg \u00fajra a jelsz\u00f3t: {felhaszn\u00e1l\u00f3n\u00e9v}", + "description": "K\u00e9rem, adja meg \u00fajra a jelsz\u00f3t: {username}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/brunt/translations/ja.json b/homeassistant/components/brunt/translations/ja.json index a0c477443b8..ac50c52ee09 100644 --- a/homeassistant/components/brunt/translations/ja.json +++ b/homeassistant/components/brunt/translations/ja.json @@ -15,14 +15,14 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {username}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "title": "Brunt\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Brunt\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/brunt/translations/pt.json b/homeassistant/components/brunt/translations/pt.json new file mode 100644 index 00000000000..6f18afa4df3 --- /dev/null +++ b/homeassistant/components/brunt/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 7ada5b01e46..c15324825ba 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_BSBLAN_CLIENT: bsblan} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/bsblan/translations/pt.json b/homeassistant/components/bsblan/translations/pt.json index 5461f207375..3cb7a7d5891 100644 --- a/homeassistant/components/bsblan/translations/pt.json +++ b/homeassistant/components/bsblan/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" diff --git a/homeassistant/components/buienradar/__init__.py b/homeassistant/components/buienradar/__init__.py index 64b94cfbaa8..9055220b7ee 100644 --- a/homeassistant/components/buienradar/__init__.py +++ b/homeassistant/components/buienradar/__init__.py @@ -13,7 +13,7 @@ PLATFORMS = [Platform.CAMERA, Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up buienradar from a config entry.""" hass.data.setdefault(DOMAIN, {}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True diff --git a/homeassistant/components/buienradar/translations/pt.json b/homeassistant/components/buienradar/translations/pt.json new file mode 100644 index 00000000000..2e6515edd09 --- /dev/null +++ b/homeassistant/components/buienradar/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 432b6943473..54da2a1cb02 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -14,6 +14,7 @@ from homeassistant.components import frontend, http from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -222,8 +223,9 @@ class CalendarEventDevice(Entity): "description": event["description"], } + @final @property - def state(self) -> str | None: + def state(self) -> str: """Return the state of the calendar event.""" if (event := self.event) is None: return STATE_OFF @@ -276,8 +278,9 @@ class CalendarEntity(Entity): "description": event.description if event.description else "", } + @final @property - def state(self) -> str | None: + def state(self) -> str: """Return the state of the calendar event.""" if (event := self.event) is None: return STATE_OFF @@ -334,9 +337,14 @@ class CalendarEventView(http.HomeAssistantView): if not isinstance(entity, CalendarEntity): return web.Response(status=HTTPStatus.BAD_REQUEST) - calendar_event_list = await entity.async_get_events( - request.app["hass"], start_date, end_date - ) + try: + calendar_event_list = await entity.async_get_events( + request.app["hass"], start_date, end_date + ) + except HomeAssistantError as err: + return self.json_message( + f"Error reading events: {err}", HTTPStatus.INTERNAL_SERVER_ERROR + ) return self.json( [ { diff --git a/homeassistant/components/calendar/recorder.py b/homeassistant/components/calendar/recorder.py new file mode 100644 index 00000000000..4aba7b409cc --- /dev/null +++ b/homeassistant/components/calendar/recorder.py @@ -0,0 +1,10 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude potentially large attributes from being recorded in the database.""" + return {"description"} diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 45b77ec1bd6..77bd0b57f1c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -import base64 import collections from collections.abc import Awaitable, Callable, Iterable from contextlib import suppress @@ -15,7 +14,7 @@ import os from random import SystemRandom from typing import Final, Optional, cast, final -from aiohttp import web +from aiohttp import hdrs, web import async_timeout import attr import voluptuous as vol @@ -130,14 +129,6 @@ CAMERA_SERVICE_RECORD: Final = { vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), } -WS_TYPE_CAMERA_THUMBNAIL: Final = "camera_thumbnail" -SCHEMA_WS_CAMERA_THUMBNAIL: Final = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_CAMERA_THUMBNAIL, - vol.Required("entity_id"): cv.entity_id, - } -) - @dataclass class CameraEntityDescription(EntityDescription): @@ -362,12 +353,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(CameraImageView(component)) hass.http.register_view(CameraMjpegStream(component)) - websocket_api.async_register_command( - hass, - WS_TYPE_CAMERA_THUMBNAIL, - websocket_camera_thumbnail, - SCHEMA_WS_CAMERA_THUMBNAIL, - ) + websocket_api.async_register_command(hass, ws_camera_stream) websocket_api.async_register_command(hass, ws_camera_web_rtc_offer) websocket_api.async_register_command(hass, websocket_get_prefs) @@ -729,7 +715,12 @@ class CameraView(HomeAssistantView): ) if not authenticated: - raise web.HTTPUnauthorized() + # Attempt with invalid bearer token, raise unauthorized + # so ban middleware can handle it. + if hdrs.AUTHORIZATION in request.headers: + raise web.HTTPUnauthorized() + # Invalid sigAuth or camera access token + raise web.HTTPForbidden() if not camera.is_on: _LOGGER.debug("Camera is off") @@ -793,32 +784,6 @@ class CameraMjpegStream(CameraView): raise web.HTTPBadRequest() from err -@websocket_api.async_response -async def websocket_camera_thumbnail( - hass: HomeAssistant, connection: ActiveConnection, msg: dict -) -> None: - """Handle get camera thumbnail websocket command. - - Async friendly. - """ - _LOGGER.warning("The websocket command 'camera_thumbnail' has been deprecated") - try: - image = await async_get_image(hass, msg["entity_id"]) - await connection.send_big_result( - msg["id"], - { - "content_type": image.content_type, - "content": base64.b64encode(image.content).decode("utf-8"), - }, - ) - except HomeAssistantError: - connection.send_message( - websocket_api.error_message( - msg["id"], "image_fetch_failed", "Unable to fetch image" - ) - ) - - @websocket_api.websocket_command( { vol.Required("type"): "camera/stream", diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index aa4ca61e1d5..b1ab479f3a5 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,7 +3,7 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "dependencies": ["http"], - "requirements": ["PyTurboJPEG==1.6.6"], + "requirements": ["PyTurboJPEG==1.6.7"], "after_dependencies": ["media_player"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 3d54c10d09a..08c57631a1b 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -36,14 +36,14 @@ class CameraPreferences: def __init__(self, hass: HomeAssistant) -> None: """Initialize camera prefs.""" self._hass = hass - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = Store[dict[str, dict[str, bool]]]( + hass, STORAGE_VERSION, STORAGE_KEY + ) self._prefs: dict[str, dict[str, bool]] | None = None async def async_initialize(self) -> None: """Finish initializing the preferences.""" - if (prefs := await self._store.async_load()) is None or not isinstance( - prefs, dict - ): + if (prefs := await self._store.async_load()) is None: prefs = {} self._prefs = prefs diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 9b020a2f09d..e6ea20e0768 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -116,7 +116,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 44bac9e3bde..04d8d159541 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +import logging from typing import Final from aiohttp.web import Request, StreamResponse @@ -23,7 +24,7 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import Throttle +from homeassistant.util import dt as dt_util from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -34,7 +35,7 @@ from .const import ( ) from .coordinator import CanaryDataUpdateCoordinator -MIN_TIME_BETWEEN_SESSION_RENEW: Final = timedelta(seconds=90) +FORCE_CAMERA_REFRESH_INTERVAL: Final = timedelta(minutes=15) PLATFORM_SCHEMA: Final = vol.All( cv.deprecated(CONF_FFMPEG_ARGUMENTS), @@ -47,6 +48,8 @@ PLATFORM_SCHEMA: Final = vol.All( ), ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -105,6 +108,11 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): model=device.device_type["name"], name=device.name, ) + self._image: bytes | None = None + self._expires_at = dt_util.utcnow() + _LOGGER.debug( + "%s %s has been initialized", self.name, device.device_type["name"] + ) @property def location(self) -> Location: @@ -125,17 +133,33 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - await self.hass.async_add_executor_job(self.renew_live_stream_session) - live_stream_url = await self.hass.async_add_executor_job( - getattr, self._live_stream_session, "live_stream_url" - ) - return await ffmpeg.async_get_image( - self.hass, - live_stream_url, - extra_cmd=self._ffmpeg_arguments, - width=width, - height=height, - ) + utcnow = dt_util.utcnow() + if self._expires_at <= utcnow: + _LOGGER.debug("Grabbing a live view image from %s", self.name) + await self.hass.async_add_executor_job(self.renew_live_stream_session) + + if (live_stream_session := self._live_stream_session) is None: + return None + + if not (live_stream_url := live_stream_session.live_stream_url): + return None + + image = await ffmpeg.async_get_image( + self.hass, + live_stream_url, + extra_cmd=self._ffmpeg_arguments, + width=width, + height=height, + ) + + if image: + self._image = image + self._expires_at = FORCE_CAMERA_REFRESH_INTERVAL + utcnow + _LOGGER.debug("Grabbed a live view image from %s", self.name) + await self.hass.async_add_executor_job(live_stream_session.stop_session) + _LOGGER.debug("Stopped live session from %s", self.name) + + return self._image async def handle_async_mjpeg_stream( self, request: Request @@ -161,9 +185,14 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): finally: await stream.close() - @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) def renew_live_stream_session(self) -> None: """Renew live stream session.""" self._live_stream_session = self.coordinator.canary.get_live_stream_session( self._device ) + + _LOGGER.debug( + "Live Stream URL for %s is %s", + self.name, + self._live_stream_session.live_stream_url, + ) diff --git a/homeassistant/components/canary/translations/ja.json b/homeassistant/components/canary/translations/ja.json index 9f9903b86e4..1fcbcde47d1 100644 --- a/homeassistant/components/canary/translations/ja.json +++ b/homeassistant/components/canary/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index c8a6a82571e..da32dfd6ae7 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -274,6 +274,7 @@ class CastDevice: class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Representation of a Cast device on the network.""" + _attr_has_entity_name = True _attr_should_poll = False _attr_media_image_remotely_accessible = True _mz_only = False @@ -293,7 +294,6 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): self._cast_view_remove_handler = None self._attr_unique_id = str(cast_info.uuid) - self._attr_name = cast_info.friendly_name self._attr_device_info = DeviceInfo( identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, manufacturer=str(cast_info.cast_info.manufacturer), diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index 626ef56cba1..1dab27d9e54 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8\u306f\u3001\u30b3\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305f\u30db\u30b9\u30c8\u306e\u30ea\u30b9\u30c8\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002" diff --git a/homeassistant/components/cast/translations/pt.json b/homeassistant/components/cast/translations/pt.json index bb29c923128..34770733822 100644 --- a/homeassistant/components/cast/translations/pt.json +++ b/homeassistant/components/cast/translations/pt.json @@ -8,7 +8,7 @@ "title": "Google Cast" }, "confirm": { - "description": "Deseja configurar o Google Cast?" + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" } } } diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 95f1b660520..85f73532fed 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_finish_startup(_): await coordinator.async_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.state == CoreState.running: await async_finish_startup(None) diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index cf80b83fc36..4f2ad7c5889 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -159,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json index f65fa638ced..4cad1eaaa0f 100644 --- a/homeassistant/components/climacell/translations/hu.json +++ b/homeassistant/components/climacell/translations/hu.json @@ -6,7 +6,7 @@ "timestep": "Min. A NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt" }, "description": "Ha a `nowcast` el\u0151rejelz\u00e9si entit\u00e1s enged\u00e9lyez\u00e9s\u00e9t v\u00e1lasztja, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tti percek sz\u00e1m\u00e1t. A megadott el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt kiv\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.", - "title": "[%key:component::climacell::title%] be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" + "title": "ClimaCell be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" } } } diff --git a/homeassistant/components/climacell/translations/sensor.pt.json b/homeassistant/components/climacell/translations/sensor.pt.json new file mode 100644 index 00000000000..30ba0f75808 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy_for_sensitive_groups": "Pouco saud\u00e1vel para grupos sens\u00edveis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 6aee9b54f6c..73ff6361041 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -148,17 +148,16 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional state attributes.""" - wind_gust = self.wind_gust - wind_gust = round( - speed_convert(self.wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), - 4, - ) cloud_cover = self.cloud_cover - return { + attrs = { ATTR_CLOUD_COVER: cloud_cover, - ATTR_WIND_GUST: wind_gust, ATTR_PRECIPITATION_TYPE: self.precipitation_type, } + if (wind_gust := self.wind_gust) is not None: + attrs[ATTR_WIND_GUST] = round( + speed_convert(wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), 4 + ) + return attrs @property @abstractmethod diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index f7e63f475ea..0bbc6fce7ec 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -10,6 +10,7 @@ from homeassistant.core import Context, HomeAssistant, State from .const import ( ATTR_AUX_HEAT, + ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODE, ATTR_PRESET_MODE, @@ -19,6 +20,7 @@ from .const import ( DOMAIN, HVAC_MODES, SERVICE_SET_AUX_HEAT, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, @@ -70,6 +72,9 @@ async def _async_reproduce_states( if ATTR_SWING_MODE in state.attributes: await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE]) + if ATTR_FAN_MODE in state.attributes: + await call_service(SERVICE_SET_FAN_MODE, [ATTR_FAN_MODE]) + if ATTR_HUMIDITY in state.attributes: await call_service(SERVICE_SET_HUMIDITY, [ATTR_HUMIDITY]) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 5df16cb1724..19819307cf0 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -33,7 +33,20 @@ async def async_provide_implementation(hass: HomeAssistant, domain: str): services = await _get_services(hass) for service in services: - if service["service"] == domain and CURRENT_VERSION >= service["min_version"]: + if ( + service["service"] == domain + and CURRENT_VERSION >= service["min_version"] + and ( + service.get("accepts_new_authorizations", True) + or ( + (entries := hass.config_entries.async_entries(domain)) + and any( + entry.data.get("auth_implementation") == DOMAIN + for entry in entries + ) + ) + ) + ): return [CloudOAuth2Implementation(hass, domain)] return [] diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 6086cef703a..8e5c214b388 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -419,6 +419,7 @@ async def websocket_hook_delete(hass, connection, msg): async def _account_data(hass: HomeAssistant, cloud: Cloud): """Generate the auth data JSON response.""" + assert hass.config.api if not cloud.is_logged_in: return { "logged_in": False, diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 81fdf638148..08ffdd5c7a5 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { @@ -28,7 +28,7 @@ "data": { "api_token": "API\u30c8\u30fc\u30af\u30f3" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u5185\u306e\u3059\u3079\u3066\u306e\u30be\u30fc\u30f3\u306b\u5bfe\u3059\u308b\u3001 Zone:Zone:Read \u304a\u3088\u3073\u3001Zone:DNS:Edit\u306e\u6a29\u9650\u3067\u4f5c\u6210\u3055\u308c\u305fAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002", + "description": "\u3053\u306e\u7d71\u5408\u306b\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u5185\u306e\u3059\u3079\u3066\u306e\u30be\u30fc\u30f3\u306b\u5bfe\u3059\u308b\u3001 Zone:Zone:Read \u304a\u3088\u3073\u3001Zone:DNS:Edit\u306e\u6a29\u9650\u3067\u4f5c\u6210\u3055\u308c\u305fAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002", "title": "Cloudflare\u306b\u63a5\u7d9a" }, "zone": { diff --git a/homeassistant/components/cloudflare/translations/pt.json b/homeassistant/components/cloudflare/translations/pt.json index 158cd3f3f74..650823693d6 100644 --- a/homeassistant/components/cloudflare/translations/pt.json +++ b/homeassistant/components/cloudflare/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index ba613ea6b18..a5bae23332d 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -15,7 +15,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_COUNTRY_CODE, DOMAIN -from .util import get_extra_name PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -49,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -73,10 +72,6 @@ class CO2SignalCoordinator(DataUpdateCoordinator[CO2SignalResponse]): """Return entry ID.""" return self._entry.entry_id - def get_extra_name(self) -> str | None: - """Return the extra name describing the location if not home.""" - return get_extra_name(self._entry.data) - async def _async_update_data(self) -> CO2SignalResponse: """Fetch the latest data from the source.""" try: diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 23303474e3b..841848621ec 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -5,14 +5,17 @@ from dataclasses import dataclass from datetime import timedelta from typing import cast -from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import CO2SignalCoordinator @@ -22,12 +25,9 @@ SCAN_INTERVAL = timedelta(minutes=3) @dataclass -class CO2SensorEntityDescription: +class CO2SensorEntityDescription(SensorEntityDescription): """Provide a description of a CO2 sensor.""" - key: str - name: str - unit_of_measurement: str | None = None # For backwards compat, allow description to override unique ID key to use unique_id: str | None = None @@ -42,7 +42,7 @@ SENSORS = ( CO2SensorEntityDescription( key="fossilFuelPercentage", name="Grid fossil fuel percentage", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), ) @@ -58,21 +58,18 @@ async def async_setup_entry( class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): """Implementation of the CO2Signal sensor.""" - _attr_state_class = SensorStateClass.MEASUREMENT + entity_description: CO2SensorEntityDescription + _attr_has_entity_name = True _attr_icon = "mdi:molecule-co2" + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, coordinator: CO2SignalCoordinator, description: CO2SensorEntityDescription ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._description = description + self.entity_description = description - name = description.name - if extra_name := coordinator.get_extra_name(): - name = f"{extra_name} - {name}" - - self._attr_name = name self._attr_extra_state_attributes = { "country_code": coordinator.data["countryCode"], ATTR_ATTRIBUTION: ATTRIBUTION, @@ -92,19 +89,22 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): def available(self) -> bool: """Return True if entity is available.""" return ( - super().available and self._description.key in self.coordinator.data["data"] + super().available + and self.entity_description.key in self.coordinator.data["data"] ) @property - def native_value(self) -> StateType: + def native_value(self) -> float | None: """Return sensor state.""" - if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[literal-required] + if (value := self.coordinator.data["data"][self.entity_description.key]) is None: # type: ignore[literal-required] return None return round(value, 2) @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" - if self._description.unit_of_measurement: - return self._description.unit_of_measurement - return cast(str, self.coordinator.data["units"].get(self._description.key)) + if self.entity_description.native_unit_of_measurement: + return self.entity_description.native_unit_of_measurement + return cast( + str, self.coordinator.data["units"].get(self.entity_description.key) + ) diff --git a/homeassistant/components/co2signal/translations/pt.json b/homeassistant/components/co2signal/translations/pt.json new file mode 100644 index 00000000000..6d105e40d36 --- /dev/null +++ b/homeassistant/components/co2signal/translations/pt.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + }, + "country": { + "data": { + "country_code": "C\u00f3digo do Pa\u00eds" + } + }, + "user": { + "data": { + "api_key": "Token de Acesso", + "location": "Obter dados para" + }, + "description": "Visite https://co2signal.com/ para solicitar um token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 4ef26a11130..36ce65517db 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = instance - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/coinbase/translations/hu.json b/homeassistant/components/coinbase/translations/hu.json index bcf409d2dda..54122d29966 100644 --- a/homeassistant/components/coinbase/translations/hu.json +++ b/homeassistant/components/coinbase/translations/hu.json @@ -16,7 +16,7 @@ "api_key": "API kulcs", "api_token": "API jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg API kulcs\u00e1nak adatait a Coinbase \u00e1ltal megadott m\u00f3don.", + "description": "K\u00e9rem, adja meg API kulcs\u00e1nak adatait a Coinbase \u00e1ltal megadott m\u00f3don.", "title": "Coinbase API kulcs r\u00e9szletei" } } diff --git a/homeassistant/components/coinbase/translations/pt.json b/homeassistant/components/coinbase/translations/pt.json new file mode 100644 index 00000000000..49cb628dd85 --- /dev/null +++ b/homeassistant/components/coinbase/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 1bcaaaa60a6..172c321c0ec 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -23,7 +23,11 @@ def call_shell_with_timeout( return 0 except subprocess.CalledProcessError as proc_exception: if log_return_code: - _LOGGER.error("Command failed: %s", command) + _LOGGER.error( + "Command failed (with return code %s): %s", + proc_exception.returncode, + command, + ) return proc_exception.returncode except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", command) @@ -40,8 +44,10 @@ def check_output_or_log(command: str, timeout: int) -> str | None: command, shell=True, timeout=timeout # nosec # shell by design ) return return_value.strip().decode("utf-8") - except subprocess.CalledProcessError: - _LOGGER.error("Command failed: %s", command) + except subprocess.CalledProcessError as err: + _LOGGER.error( + "Command failed (with return code %s): %s", err.returncode, command + ) except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", command) except subprocess.SubprocessError: diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 609166f2d16..8298201228f 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -115,10 +115,13 @@ class CommandCover(CoverEntity): """Execute the actual commands.""" _LOGGER.info("Running command: %s", command) - success = call_shell_with_timeout(command, self._timeout) == 0 + returncode = call_shell_with_timeout(command, self._timeout) + success = returncode == 0 if not success: - _LOGGER.error("Command failed: %s", command) + _LOGGER.error( + "Command failed (with return code %s): %s", returncode, command + ) return success diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 6f364947775..7bce5010d45 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -57,7 +57,11 @@ class CommandLineNotificationService(BaseNotificationService): try: proc.communicate(input=message, timeout=self._timeout) if proc.returncode != 0: - _LOGGER.error("Command failed: %s", self.command) + _LOGGER.error( + "Command failed (with return code %s): %s", + proc.returncode, + self.command, + ) except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", self.command) kill_subprocess(proc) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 509d5740b22..5b1ff9715d1 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.23.0"], + "requirements": ["numpy==1.23.1"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index e811d43d502..587710a8c2a 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,12 +1,23 @@ """HTTP views to interact with the device registry.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import loader from homeassistant.components import websocket_api from homeassistant.components.websocket_api.decorators import require_admin -from homeassistant.core import HomeAssistant, callback +from homeassistant.components.websocket_api.messages import ( + IDEN_JSON_TEMPLATE, + IDEN_TEMPLATE, + message_to_json, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceEntryDisabler, async_get +from homeassistant.helpers.device_registry import ( + EVENT_DEVICE_REGISTRY_UPDATED, + DeviceEntryDisabler, + async_get, +) WS_TYPE_LIST = "config/device_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -29,6 +40,36 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( async def async_setup(hass): """Enable the Device Registry views.""" + + cached_list_devices: str | None = None + + @callback + def _async_clear_list_device_cache(event: Event) -> None: + nonlocal cached_list_devices + cached_list_devices = None + + @callback + def websocket_list_devices(hass, connection, msg): + """Handle list devices command.""" + nonlocal cached_list_devices + if not cached_list_devices: + registry = async_get(hass) + cached_list_devices = message_to_json( + websocket_api.result_message( + IDEN_TEMPLATE, + [_entry_dict(entry) for entry in registry.devices.values()], + ) + ) + connection.send_message( + cached_list_devices.replace(IDEN_JSON_TEMPLATE, str(msg["id"]), 1) + ) + + hass.bus.async_listen( + EVENT_DEVICE_REGISTRY_UPDATED, + _async_clear_list_device_cache, + run_immediately=True, + ) + websocket_api.async_register_command( hass, WS_TYPE_LIST, websocket_list_devices, SCHEMA_WS_LIST ) @@ -41,17 +82,6 @@ async def async_setup(hass): return True -@callback -def websocket_list_devices(hass, connection, msg): - """Handle list devices command.""" - registry = async_get(hass) - connection.send_message( - websocket_api.result_message( - msg["id"], [_entry_dict(entry) for entry in registry.devices.values()] - ) - ) - - @require_admin @callback def websocket_update_device(hass, connection, msg): diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index e6b91ee5a50..6d022aa2d14 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,11 +1,20 @@ """HTTP views to interact with the entity registry.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import require_admin -from homeassistant.core import callback +from homeassistant.components.websocket_api.messages import ( + IDEN_JSON_TEMPLATE, + IDEN_TEMPLATE, + message_to_json, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -13,8 +22,40 @@ from homeassistant.helpers import ( ) -async def async_setup(hass): +async def async_setup(hass: HomeAssistant) -> bool: """Enable the Entity Registry views.""" + + cached_list_entities: str | None = None + + @callback + def _async_clear_list_entities_cache(event: Event) -> None: + nonlocal cached_list_entities + cached_list_entities = None + + @websocket_api.websocket_command( + {vol.Required("type"): "config/entity_registry/list"} + ) + @callback + def websocket_list_entities(hass, connection, msg): + """Handle list registry entries command.""" + nonlocal cached_list_entities + if not cached_list_entities: + registry = er.async_get(hass) + cached_list_entities = message_to_json( + websocket_api.result_message( + IDEN_TEMPLATE, + [_entry_dict(entry) for entry in registry.entities.values()], + ) + ) + connection.send_message( + cached_list_entities.replace(IDEN_JSON_TEMPLATE, str(msg["id"]), 1) + ) + + hass.bus.async_listen( + er.EVENT_ENTITY_REGISTRY_UPDATED, + _async_clear_list_entities_cache, + run_immediately=True, + ) websocket_api.async_register_command(hass, websocket_list_entities) websocket_api.async_register_command(hass, websocket_get_entity) websocket_api.async_register_command(hass, websocket_update_entity) @@ -22,18 +63,6 @@ async def async_setup(hass): return True -@websocket_api.websocket_command({vol.Required("type"): "config/entity_registry/list"}) -@callback -def websocket_list_entities(hass, connection, msg): - """Handle list registry entries command.""" - registry = er.async_get(hass) - connection.send_message( - websocket_api.result_message( - msg["id"], [_entry_dict(entry) for entry in registry.entities.values()] - ) - ) - - @websocket_api.websocket_command( { vol.Required("type"): "config/entity_registry/get", @@ -196,32 +225,32 @@ def websocket_remove_entity(hass, connection, msg): @callback -def _entry_dict(entry): +def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: """Convert entry to API format.""" return { "area_id": entry.area_id, "config_entry_id": entry.config_entry_id, "device_id": entry.device_id, "disabled_by": entry.disabled_by, + "has_entity_name": entry.has_entity_name, "entity_category": entry.entity_category, "entity_id": entry.entity_id, "hidden_by": entry.hidden_by, "icon": entry.icon, "name": entry.name, + "original_name": entry.original_name, "platform": entry.platform, } @callback -def _entry_ext_dict(entry): +def _entry_ext_dict(entry: er.RegistryEntry) -> dict[str, Any]: """Convert entry to API format.""" data = _entry_dict(entry) data["capabilities"] = entry.capabilities data["device_class"] = entry.device_class - data["has_entity_name"] = entry.has_entity_name data["options"] = entry.options data["original_device_class"] = entry.original_device_class data["original_icon"] = entry.original_icon - data["original_name"] = entry.original_name data["unique_id"] = entry.unique_id return data diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 48a639e56f6..4253a4eca02 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data[CONF_CONFIG_LISTENER] = entry.add_update_listener(update_listener) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json index 5d41eb09a84..033555af95e 100644 --- a/homeassistant/components/control4/translations/hu.json +++ b/homeassistant/components/control4/translations/hu.json @@ -15,7 +15,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg Control4-fi\u00f3kj\u00e1nak adatait \u00e9s a helyi vez\u00e9rl\u0151 IP-c\u00edm\u00e9t." + "description": "K\u00e9rem, adja meg Control4-fi\u00f3kj\u00e1nak adatait \u00e9s a helyi vez\u00e9rl\u0151 IP-c\u00edm\u00e9t." } } }, diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index d04878cae4e..9fd6d1ad3e2 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -80,10 +80,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@websocket_api.async_response @websocket_api.websocket_command( {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str} ) +@websocket_api.async_response async def websocket_process(hass, connection, msg): """Process text.""" connection.send_result( @@ -94,8 +94,8 @@ async def websocket_process(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command({"type": "conversation/agent/info"}) +@websocket_api.async_response async def websocket_get_agent_info(hass, connection, msg): """Do we need onboarding.""" agent = await _get_agent(hass) @@ -109,8 +109,8 @@ async def websocket_get_agent_info(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) +@websocket_api.async_response async def websocket_set_onboarding(hass, connection, msg): """Set onboarding status.""" agent = await _get_agent(hass) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 56cf4aecdea..a19ae6d697b 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -26,5 +26,5 @@ class AbstractConversationAgent(ABC): @abstractmethod async def async_process( self, text: str, context: Context, conversation_id: str | None = None - ) -> intent.IntentResponse: + ) -> intent.IntentResponse | None: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 6d8d29ab086..9079f7893ec 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -112,7 +112,7 @@ class DefaultAgent(AbstractConversationAgent): async def async_process( self, text: str, context: core.Context, conversation_id: str | None = None - ) -> intent.IntentResponse: + ) -> intent.IntentResponse | None: """Process a sentence.""" intents = self.hass.data[DOMAIN] @@ -129,3 +129,5 @@ class DefaultAgent(AbstractConversationAgent): text, context, ) + + return None diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 0f98f8d3afc..ef2b5328f96 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_INFO: info, DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index 27085c88ef2..a1c4f876f66 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not coordinator.last_update_success: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/cpuspeed/__init__.py b/homeassistant/components/cpuspeed/__init__.py index a5d6a35459f..da1e0129117 100644 --- a/homeassistant/components/cpuspeed/__init__.py +++ b/homeassistant/components/cpuspeed/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 2c9db7e7300..6b64b8709a3 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -7,8 +7,11 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import FREQUENCY_GIGAHERTZ from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import DOMAIN + ATTR_BRAND = "brand" ATTR_HZ = "ghz_advertised" ATTR_ARCH = "arch" @@ -30,12 +33,16 @@ class CPUSpeedSensor(SensorEntity): """Representation of a CPU sensor.""" _attr_icon = "mdi:pulse" - _attr_name = "CPU Speed" + _attr_has_entity_name = True _attr_native_unit_of_measurement = FREQUENCY_GIGAHERTZ def __init__(self, entry: ConfigEntry) -> None: """Initialize the CPU sensor.""" self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + name="CPU Speed", + identifiers={(DOMAIN, entry.entry_id)}, + ) def update(self) -> None: """Get the latest data and updates the state.""" diff --git a/homeassistant/components/cpuspeed/translations/ja.json b/homeassistant/components/cpuspeed/translations/ja.json index e2264b57dcb..95e92eafa76 100644 --- a/homeassistant/components/cpuspeed/translations/ja.json +++ b/homeassistant/components/cpuspeed/translations/ja.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", - "not_compatible": "CPU\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u304a\u4f7f\u3044\u306e\u30b7\u30b9\u30c6\u30e0\u3068\u4e92\u63db\u6027\u304c\u3042\u308a\u307e\u305b\u3093\u3002" + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", + "not_compatible": "CPU\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u7d71\u5408\u306f\u3001\u304a\u4f7f\u3044\u306e\u30b7\u30b9\u30c6\u30e0\u3068\u4e92\u63db\u6027\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, "step": { "user": { diff --git a/homeassistant/components/cpuspeed/translations/pt.json b/homeassistant/components/cpuspeed/translations/pt.json new file mode 100644 index 00000000000..cf03f249d96 --- /dev/null +++ b/homeassistant/components/cpuspeed/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/entry_manager.py b/homeassistant/components/crownstone/entry_manager.py index b1963462adc..dcae7ef4705 100644 --- a/homeassistant/components/crownstone/entry_manager.py +++ b/homeassistant/components/crownstone/entry_manager.py @@ -97,7 +97,9 @@ class CrownstoneEntryManager: # Makes HA aware of the Crownstone environment HA is placed in, a user can have multiple self.usb_sphere_id = self.config_entry.options[CONF_USB_SPHERE] - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) # HA specific listeners self.config_entry.async_on_unload( diff --git a/homeassistant/components/crownstone/translations/hu.json b/homeassistant/components/crownstone/translations/hu.json index f2ffe6f2b07..1d7c6efe2ea 100644 --- a/homeassistant/components/crownstone/translations/hu.json +++ b/homeassistant/components/crownstone/translations/hu.json @@ -6,7 +6,7 @@ "usb_setup_unsuccessful": "A Crownstone USB be\u00e1ll\u00edt\u00e1sa sikertelen volt." }, "error": { - "account_not_verified": "Nem ellen\u0151rz\u00f6tt fi\u00f3k. K\u00e9rj\u00fck, aktiv\u00e1lja fi\u00f3kj\u00e1t a Crownstone-t\u00f3l kapott aktiv\u00e1l\u00f3 e-mailben.", + "account_not_verified": "Nem ellen\u0151rz\u00f6tt fi\u00f3k. K\u00e9rem, aktiv\u00e1lja fi\u00f3kj\u00e1t a Crownstone-t\u00f3l kapott aktiv\u00e1l\u00f3 e-mailben.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/crownstone/translations/pt.json b/homeassistant/components/crownstone/translations/pt.json new file mode 100644 index 00000000000..97ea705b32b --- /dev/null +++ b/homeassistant/components/crownstone/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "usb_manual_config": { + "data": { + "usb_manual_path": "Caminho do Dispositivo USB" + } + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 5f05355374c..536e2fa48d1 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 00e4839be63..3cc6c16e831 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.6.0"], + "requirements": ["debugpy==1.6.2"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 112f29db333..cb4715f3c28 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -46,12 +46,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway( hass, config_entry, api ) + await gateway.async_update_device_registry() config_entry.add_update_listener(gateway.async_config_entry_updated) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) await async_setup_events(gateway) - await gateway.async_update_device_registry() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if len(hass.data[DOMAIN]) == 1: async_setup_services(hass) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 0d090751edd..814dec443e0 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -24,7 +24,6 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.entity_registry as er @@ -215,9 +214,6 @@ async def async_setup_entry( """Add sensor from deCONZ.""" sensor = gateway.api.sensors[sensor_id] - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - return - for description in ( ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS ): @@ -236,21 +232,6 @@ async def async_setup_entry( gateway.api.sensors, ) - @callback - def async_reload_clip_sensors() -> None: - """Load clip sensor sensors from deCONZ.""" - for sensor_id, sensor in gateway.api.sensors.items(): - if sensor.type.startswith("CLIP"): - async_add_sensor(EventType.ADDED, sensor_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_clip_sensors, - async_reload_clip_sensors, - ) - ) - class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ binary sensor.""" diff --git a/homeassistant/components/deconz/button.py b/homeassistant/components/deconz/button.py index 498c88a2351..552723d6f9c 100644 --- a/homeassistant/components/deconz/button.py +++ b/homeassistant/components/deconz/button.py @@ -90,8 +90,11 @@ class DeconzButton(DeconzSceneMixin, ButtonEntity): async def async_press(self) -> None: """Store light states into scene.""" - async_button_fn = getattr(self._device, self.entity_description.button_fn) - await async_button_fn() + async_button_fn = getattr( + self.gateway.api.scenes, + self.entity_description.button_fn, + ) + await async_button_fn(self._device.group_id, self._device.id) def get_device_identifier(self) -> str: """Return a unique identifier for this scene.""" diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 880e11f080b..75b37db2c13 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -28,7 +28,6 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE @@ -85,8 +84,6 @@ async def async_setup_entry( def async_add_climate(_: EventType, climate_id: str) -> None: """Add climate from deCONZ.""" climate = gateway.api.sensors.thermostat[climate_id] - if not gateway.option_allow_clip_sensor and climate.type.startswith("CLIP"): - return async_add_entities([DeconzThermostat(climate, gateway)]) gateway.register_platform_add_device_callback( @@ -94,21 +91,6 @@ async def async_setup_entry( gateway.api.sensors.thermostat, ) - @callback - def async_reload_clip_sensors() -> None: - """Load clip sensors from deCONZ.""" - for climate_id, climate in gateway.api.sensors.thermostat.items(): - if climate.type.startswith("CLIP"): - async_add_climate(EventType.ADDED, climate_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_clip_sensors, - async_reload_clip_sensors, - ) - ) - class DeconzThermostat(DeconzDevice, ClimateEntity): """Representation of a deCONZ thermostat.""" diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 9efbeac366f..3e56882f15a 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any, cast +from pydeconz.interfaces.lights import CoverAction from pydeconz.models.event import EventType from pydeconz.models.light.cover import Cover @@ -21,7 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -DEVICE_CLASS = { +DECONZ_TYPE_TO_DEVICE_CLASS = { "Level controllable output": CoverDeviceClass.DAMPER, "Window covering controller": CoverDeviceClass.SHADE, "Window covering device": CoverDeviceClass.SHADE, @@ -40,8 +41,7 @@ async def async_setup_entry( @callback def async_add_cover(_: EventType, cover_id: str) -> None: """Add cover from deCONZ.""" - cover = gateway.api.lights.covers[cover_id] - async_add_entities([DeconzCover(cover, gateway)]) + async_add_entities([DeconzCover(cover_id, gateway)]) gateway.register_platform_add_device_callback( async_add_cover, @@ -55,9 +55,9 @@ class DeconzCover(DeconzDevice, CoverEntity): TYPE = DOMAIN _device: Cover - def __init__(self, device: Cover, gateway: DeconzGateway) -> None: + def __init__(self, cover_id: str, gateway: DeconzGateway) -> None: """Set up cover device.""" - super().__init__(device, gateway) + super().__init__(cover := gateway.api.lights.covers[cover_id], gateway) self._attr_supported_features = CoverEntityFeature.OPEN self._attr_supported_features |= CoverEntityFeature.CLOSE @@ -70,7 +70,7 @@ class DeconzCover(DeconzDevice, CoverEntity): self._attr_supported_features |= CoverEntityFeature.STOP_TILT self._attr_supported_features |= CoverEntityFeature.SET_TILT_POSITION - self._attr_device_class = DEVICE_CLASS.get(self._device.type) + self._attr_device_class = DECONZ_TYPE_TO_DEVICE_CLASS.get(cover.type) @property def current_cover_position(self) -> int: @@ -85,19 +85,31 @@ class DeconzCover(DeconzDevice, CoverEntity): async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = 100 - cast(int, kwargs[ATTR_POSITION]) - await self._device.set_position(lift=position) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + lift=position, + ) async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" - await self._device.open() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.OPEN, + ) async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - await self._device.close() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.CLOSE, + ) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" - await self._device.stop() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.STOP, + ) @property def current_cover_tilt_position(self) -> int | None: @@ -109,16 +121,28 @@ class DeconzCover(DeconzDevice, CoverEntity): async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Tilt the cover to a specific position.""" position = 100 - cast(int, kwargs[ATTR_TILT_POSITION]) - await self._device.set_position(tilt=position) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + tilt=position, + ) async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open cover tilt.""" - await self._device.set_position(tilt=0) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + tilt=0, + ) async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close cover tilt.""" - await self._device.set_position(tilt=100) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + tilt=100, + ) async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop cover tilt.""" - await self._device.stop() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.STOP, + ) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 94051cbf0ac..e9be4658fb5 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -120,6 +120,8 @@ class DeconzDevice(DeconzBase, Entity): class DeconzSceneMixin(DeconzDevice): """Representation of a deCONZ scene.""" + _attr_has_entity_name = True + _device: PydeconzScene def __init__( @@ -130,7 +132,9 @@ class DeconzSceneMixin(DeconzDevice): """Set up a scene.""" super().__init__(device, gateway) - self._attr_name = device.full_name + self.group = self.gateway.api.groups[device.group_id] + + self._attr_name = device.name self._group_identifier = self.get_parent_identifier() def get_device_identifier(self) -> str: @@ -139,7 +143,7 @@ class DeconzSceneMixin(DeconzDevice): def get_parent_identifier(self) -> str: """Describe a unique identifier for group this scene belongs to.""" - return f"{self.gateway.bridgeid}-{self._device.group_deconz_id}" + return f"{self.gateway.bridgeid}-{self.group.deconz_id}" @property def unique_id(self) -> str: @@ -149,4 +153,10 @@ class DeconzSceneMixin(DeconzDevice): @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return DeviceInfo(identifiers={(DECONZ_DOMAIN, self._group_identifier)}) + return DeviceInfo( + identifiers={(DECONZ_DOMAIN, self._group_identifier)}, + manufacturer="Dresden Elektronik", + model="deCONZ group", + name=self.group.name, + via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + ) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 270e66bf91d..6f7b6f9038a 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -46,9 +46,6 @@ async def async_setup_events(gateway: DeconzGateway) -> None: new_event: DeconzAlarmEvent | DeconzEvent sensor = gateway.api.sensors[sensor_id] - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - return None - if isinstance(sensor, Switch): new_event = DeconzEvent(sensor, gateway) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index ab9a1ba6f4a..6002e61d326 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -1,17 +1,10 @@ """Support for deCONZ fans.""" from __future__ import annotations -from typing import Any, Literal +from typing import Any from pydeconz.models.event import EventType -from pydeconz.models.light.fan import ( - FAN_SPEED_25_PERCENT, - FAN_SPEED_50_PERCENT, - FAN_SPEED_75_PERCENT, - FAN_SPEED_100_PERCENT, - FAN_SPEED_OFF, - Fan, -) +from pydeconz.models.light.light import Light, LightFanSpeed from homeassistant.components.fan import DOMAIN, FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry @@ -25,11 +18,11 @@ from homeassistant.util.percentage import ( from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -ORDERED_NAMED_FAN_SPEEDS: list[Literal[0, 1, 2, 3, 4, 5, 6]] = [ - FAN_SPEED_25_PERCENT, - FAN_SPEED_50_PERCENT, - FAN_SPEED_75_PERCENT, - FAN_SPEED_100_PERCENT, +ORDERED_NAMED_FAN_SPEEDS: list[LightFanSpeed] = [ + LightFanSpeed.PERCENT_25, + LightFanSpeed.PERCENT_50, + LightFanSpeed.PERCENT_75, + LightFanSpeed.PERCENT_100, ] @@ -45,12 +38,14 @@ async def async_setup_entry( @callback def async_add_fan(_: EventType, fan_id: str) -> None: """Add fan from deCONZ.""" - fan = gateway.api.lights.fans[fan_id] + fan = gateway.api.lights.lights[fan_id] + if not fan.supports_fan_speed: + return async_add_entities([DeconzFan(fan, gateway)]) gateway.register_platform_add_device_callback( async_add_fan, - gateway.api.lights.fans, + gateway.api.lights.lights, ) @@ -58,33 +53,32 @@ class DeconzFan(DeconzDevice, FanEntity): """Representation of a deCONZ fan.""" TYPE = DOMAIN - _device: Fan - _default_on_speed: Literal[0, 1, 2, 3, 4, 5, 6] + _device: Light + _default_on_speed = LightFanSpeed.PERCENT_50 _attr_supported_features = FanEntityFeature.SET_SPEED - def __init__(self, device: Fan, gateway: DeconzGateway) -> None: + def __init__(self, device: Light, gateway: DeconzGateway) -> None: """Set up fan.""" super().__init__(device, gateway) - self._default_on_speed = FAN_SPEED_50_PERCENT - if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: - self._default_on_speed = self._device.speed + if device.fan_speed in ORDERED_NAMED_FAN_SPEEDS: + self._default_on_speed = device.fan_speed @property def is_on(self) -> bool: """Return true if fan is on.""" - return self._device.speed != FAN_SPEED_OFF + return self._device.fan_speed != LightFanSpeed.OFF @property def percentage(self) -> int | None: """Return the current speed percentage.""" - if self._device.speed == FAN_SPEED_OFF: + if self._device.fan_speed == LightFanSpeed.OFF: return 0 - if self._device.speed not in ORDERED_NAMED_FAN_SPEEDS: + if self._device.fan_speed not in ORDERED_NAMED_FAN_SPEEDS: return None return ordered_list_item_to_percentage( - ORDERED_NAMED_FAN_SPEEDS, self._device.speed + ORDERED_NAMED_FAN_SPEEDS, self._device.fan_speed ) @property @@ -95,16 +89,19 @@ class DeconzFan(DeconzDevice, FanEntity): @callback def async_update_callback(self) -> None: """Store latest configured speed from the device.""" - if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: - self._default_on_speed = self._device.speed + if self._device.fan_speed in ORDERED_NAMED_FAN_SPEEDS: + self._default_on_speed = self._device.fan_speed super().async_update_callback() async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" if percentage == 0: return await self.async_turn_off() - await self._device.set_speed( - percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + fan_speed=percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ), ) async def async_turn_on( @@ -117,8 +114,14 @@ class DeconzFan(DeconzDevice, FanEntity): if percentage is not None: await self.async_set_percentage(percentage) return - await self._device.set_speed(self._default_on_speed) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + fan_speed=self._default_on_speed, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off fan.""" - await self._device.set_speed(FAN_SPEED_OFF) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + fan_speed=LightFanSpeed.OFF, + ) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 25471d1448a..f8e4548cf91 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -9,8 +9,9 @@ from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors -from pydeconz.interfaces.api import APIItems, GroupedAPIItems -from pydeconz.interfaces.groups import Groups +from pydeconz.interfaces import sensors +from pydeconz.interfaces.api_handlers import APIHandler, GroupedAPIHandler +from pydeconz.interfaces.groups import GroupHandler from pydeconz.models.event import EventType from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry @@ -42,6 +43,35 @@ from .errors import AuthenticationRequired, CannotConnect if TYPE_CHECKING: from .deconz_event import DeconzAlarmEvent, DeconzEvent +SENSORS = ( + sensors.SensorResourceManager, + sensors.AirPurifierHandler, + sensors.AirQualityHandler, + sensors.AlarmHandler, + sensors.AncillaryControlHandler, + sensors.BatteryHandler, + sensors.CarbonMonoxideHandler, + sensors.ConsumptionHandler, + sensors.DaylightHandler, + sensors.DoorLockHandler, + sensors.FireHandler, + sensors.GenericFlagHandler, + sensors.GenericStatusHandler, + sensors.HumidityHandler, + sensors.LightLevelHandler, + sensors.OpenCloseHandler, + sensors.PowerHandler, + sensors.PresenceHandler, + sensors.PressureHandler, + sensors.RelativeRotaryHandler, + sensors.SwitchHandler, + sensors.TemperatureHandler, + sensors.ThermostatHandler, + sensors.TimeHandler, + sensors.VibrationHandler, + sensors.WaterHandler, +) + class DeconzGateway: """Manages a single deCONZ gateway.""" @@ -60,14 +90,17 @@ class DeconzGateway: self.ignore_state_updates = False self.signal_reachable = f"deconz-reachable-{config_entry.entry_id}" - self.signal_reload_clip_sensors = f"deconz_reload_clip_{config_entry.entry_id}" self.deconz_ids: dict[str, str] = {} self.entities: dict[str, set[str]] = {} self.events: list[DeconzAlarmEvent | DeconzEvent] = [] - self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() + self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set() self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set() + self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() + self.option_allow_clip_sensor = self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ) self.option_allow_deconz_groups = config_entry.options.get( CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS ) @@ -90,20 +123,12 @@ class DeconzGateway: """Gateway which is used with deCONZ services without defining id.""" return cast(bool, self.config_entry.options[CONF_MASTER_GATEWAY]) - # Options - - @property - def option_allow_clip_sensor(self) -> bool: - """Allow loading clip sensor from gateway.""" - return self.config_entry.options.get( - CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR - ) - @callback def register_platform_add_device_callback( self, add_device_callback: Callable[[EventType, str], None], - deconz_device_interface: APIItems | GroupedAPIItems, + deconz_device_interface: APIHandler | GroupedAPIHandler, + always_ignore_clip_sensors: bool = False, ) -> None: """Wrap add_device_callback to check allow_new_devices option.""" @@ -123,11 +148,18 @@ class DeconzGateway: self.ignored_devices.add((async_add_device, device_id)) return - if isinstance(deconz_device_interface, Groups): + if isinstance(deconz_device_interface, GroupHandler): self.deconz_groups.add((async_add_device, device_id)) if not self.option_allow_deconz_groups: return + if isinstance(deconz_device_interface, SENSORS): + device = deconz_device_interface[device_id] + if device.type.startswith("CLIP") and not always_ignore_clip_sensors: + self.clip_sensors.add((async_add_device, device_id)) + if not self.option_allow_clip_sensor: + return + add_device_callback(EventType.ADDED, device_id) self.config_entry.async_on_unload( @@ -212,15 +244,20 @@ class DeconzGateway: # Allow CLIP sensors - if self.option_allow_clip_sensor: - async_dispatcher_send(self.hass, self.signal_reload_clip_sensors) - - else: - deconz_ids += [ - sensor.deconz_id - for sensor in self.api.sensors.values() - if sensor.type.startswith("CLIP") - ] + option_allow_clip_sensor = self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ) + if option_allow_clip_sensor != self.option_allow_clip_sensor: + self.option_allow_clip_sensor = option_allow_clip_sensor + if option_allow_clip_sensor: + for add_device, device_id in self.clip_sensors: + add_device(EventType.ADDED, device_id) + else: + deconz_ids += [ + sensor.deconz_id + for sensor in self.api.sensors.values() + if sensor.type.startswith("CLIP") + ] # Allow Groups diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 7f3a47d719d..7c6f1a0e362 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -3,16 +3,12 @@ from __future__ import annotations from typing import Any, Generic, TypedDict, TypeVar +from pydeconz.interfaces.groups import GroupHandler +from pydeconz.interfaces.lights import LightHandler from pydeconz.models import ResourceType from pydeconz.models.event import EventType from pydeconz.models.group import Group -from pydeconz.models.light import ( - ALERT_LONG, - ALERT_SHORT, - EFFECT_COLOR_LOOP, - EFFECT_NONE, -) -from pydeconz.models.light.light import Light +from pydeconz.models.light.light import Light, LightAlert, LightColorMode, LightEffect from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -42,8 +38,14 @@ from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_GROUP = "is_deconz_group" -EFFECT_TO_DECONZ = {EFFECT_COLORLOOP: EFFECT_COLOR_LOOP, "None": EFFECT_NONE} -FLASH_TO_DECONZ = {FLASH_SHORT: ALERT_SHORT, FLASH_LONG: ALERT_LONG} +EFFECT_TO_DECONZ = {EFFECT_COLORLOOP: LightEffect.COLOR_LOOP, "None": LightEffect.NONE} +FLASH_TO_DECONZ = {FLASH_SHORT: LightAlert.SHORT, FLASH_LONG: LightAlert.LONG} + +DECONZ_TO_COLOR_MODE = { + LightColorMode.CT: ColorMode.COLOR_TEMP, + LightColorMode.HS: ColorMode.HS, + LightColorMode.XY: ColorMode.XY, +} _L = TypeVar("_L", Group, Light) @@ -51,10 +53,10 @@ _L = TypeVar("_L", Group, Light) class SetStateAttributes(TypedDict, total=False): """Attributes available with set state call.""" - alert: str + alert: LightAlert brightness: int color_temperature: int - effect: str + effect: LightEffect hue: int on: bool saturation: int @@ -85,8 +87,7 @@ async def async_setup_entry( @callback def async_add_light(_: EventType, light_id: str) -> None: """Add light from deCONZ.""" - light = gateway.api.lights[light_id] - assert isinstance(light, Light) + light = gateway.api.lights.lights[light_id] if light.type in POWER_PLUGS: return @@ -97,11 +98,6 @@ async def async_setup_entry( gateway.api.lights.lights, ) - gateway.register_platform_add_device_callback( - async_add_light, - gateway.api.lights.fans, - ) - @callback def async_add_group(_: EventType, group_id: str) -> None: """Add group from deCONZ. @@ -136,7 +132,13 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): """Set up light.""" super().__init__(device, gateway) - self._attr_supported_color_modes: set[str] = set() + self.api: GroupHandler | LightHandler + if isinstance(self._device, Light): + self.api = self.gateway.api.lights.lights + elif isinstance(self._device, Group): + self.api = self.gateway.api.groups + + self._attr_supported_color_modes: set[ColorMode] = set() if device.color_temp is not None: self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) @@ -164,12 +166,8 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): @property def color_mode(self) -> str | None: """Return the color mode of the light.""" - if self._device.color_mode == "ct": - color_mode = ColorMode.COLOR_TEMP - elif self._device.color_mode == "hs": - color_mode = ColorMode.HS - elif self._device.color_mode == "xy": - color_mode = ColorMode.XY + if self._device.color_mode in DECONZ_TO_COLOR_MODE: + color_mode = DECONZ_TO_COLOR_MODE[self._device.color_mode] elif self._device.brightness is not None: color_mode = ColorMode.BRIGHTNESS else: @@ -235,7 +233,7 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): if ATTR_EFFECT in kwargs and kwargs[ATTR_EFFECT] in EFFECT_TO_DECONZ: data["effect"] = EFFECT_TO_DECONZ[kwargs[ATTR_EFFECT]] - await self._device.set_state(**data) + await self.api.set_state(id=self._device.resource_id, **data) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" @@ -252,7 +250,7 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): data["alert"] = FLASH_TO_DECONZ[kwargs[ATTR_FLASH]] del data["on"] - await self._device.set_state(**data) + await self.api.set_state(id=self._device.resource_id, **data) @property def extra_state_attributes(self) -> dict[str, bool]: @@ -289,6 +287,8 @@ class DeconzLight(DeconzBaseLight[Light]): class DeconzGroup(DeconzBaseLight[Group]): """Representation of a deCONZ group.""" + _attr_has_entity_name = True + _device: Group def __init__(self, device: Group, gateway: DeconzGateway) -> None: @@ -296,6 +296,8 @@ class DeconzGroup(DeconzBaseLight[Group]): self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}" super().__init__(device, gateway) + self._attr_name = None + @property def unique_id(self) -> str: """Return a unique identifier for this device.""" diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index cf4bd7f14f5..d6f9d670c01 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -46,6 +46,7 @@ async def async_setup_entry( gateway.register_platform_add_device_callback( async_add_lock_from_sensor, gateway.api.sensors.door_lock, + always_ignore_clip_sensors=True, ) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 2ae400bbe19..6384ebfcd5f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==98"], + "requirements": ["pydeconz==101"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 81f3e434007..4c0959f950d 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -65,8 +65,7 @@ async def async_setup_entry( def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add sensor from deCONZ.""" sensor = gateway.api.sensors.presence[sensor_id] - if sensor.type.startswith("CLIP"): - return + for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): if ( not hasattr(sensor, description.key) @@ -78,6 +77,7 @@ async def async_setup_entry( gateway.register_platform_add_device_callback( async_add_sensor, gateway.api.sensors.presence, + always_ignore_clip_sensors=True, ) @@ -113,8 +113,10 @@ class DeconzNumber(DeconzDevice, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set sensor config.""" - data = {self.entity_description.key: int(value)} - await self._device.set_config(**data) + await self.gateway.api.sensors.presence.set_config( + id=self._device.resource_id, + delay=int(value), + ) @property def unique_id(self) -> str: diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 2aff2b12448..15af1b3dd8f 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -39,7 +39,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -248,9 +247,6 @@ async def async_setup_entry( sensor = gateway.api.sensors[sensor_id] entities: list[DeconzSensor] = [] - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - return - if sensor.battery is None and not sensor.type.startswith("CLIP"): DeconzBatteryTracker(sensor_id, gateway, async_add_entities) @@ -276,21 +272,6 @@ async def async_setup_entry( gateway.api.sensors, ) - @callback - def async_reload_clip_sensors() -> None: - """Load clip sensor sensors from deCONZ.""" - for sensor_id, sensor in gateway.api.sensors.items(): - if sensor.type.startswith("CLIP"): - async_add_sensor(EventType.ADDED, sensor_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_clip_sensors, - async_reload_clip_sensors, - ) - ) - class DeconzSensor(DeconzDevice, SensorEntity): """Representation of a deCONZ sensor.""" diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 8e6c6a71a44..b8afd28926c 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -68,6 +68,7 @@ "remote_button_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku", "remote_button_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", "remote_button_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t", + "remote_button_rotated": "Tla\u010d\u00edtko \"{subtype}\" se oto\u010dilo", "remote_button_rotation_stopped": "Oto\u010den\u00ed tla\u010d\u00edtka \"{subtype}\" bylo zastaveno", "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index 29b322466d5..b8b260709e8 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -18,7 +18,7 @@ "title": "deCONZ Zigbee Gateway \u00fcber das Supervisor Add-on" }, "link": { - "description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", + "description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \u2192 Gateway \u2192 Erweitert\n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", "title": "Mit deCONZ verbinden" }, "manual_input": { diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index 94efb5c68ca..53dfd5b29bf 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -54,6 +54,7 @@ "remote_button_double_press": "Bot\u00e3o \"{subtype}\" clicado duas vezes", "remote_button_long_press": "Bot\u00e3o \"{subtype}\" pressionado continuamente", "remote_falling": "Dispositivo em queda livre", + "remote_flip_180_degrees": "Dispositivo virado 180 graus", "remote_gyro_activated": "Dispositivo agitado" } }, diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 1742092cc70..f790292c27a 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -5,11 +5,13 @@ "dependencies": [ "application_credentials", "automation", + "bluetooth", "cloud", "counter", "dhcp", "energy", "frontend", + "homeassistant_alerts", "history", "input_boolean", "input_button", diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index 2253eee43d5..566b97b5b04 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = DelugeDataUpdateCoordinator(hass, api, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -73,6 +73,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class DelugeEntity(CoordinatorEntity[DelugeDataUpdateCoordinator]): """Representation of a Deluge entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: """Initialize a Deluge entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 8a8e8f64657..bcdca8b3d92 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -52,14 +52,14 @@ SENSOR_TYPES: tuple[DelugeSensorEntityDescription, ...] = ( ), DelugeSensorEntityDescription( key=DOWNLOAD_SPEED, - name="Down Speed", + name="Down speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, value=lambda data: get_state(data, DOWNLOAD_SPEED), ), DelugeSensorEntityDescription( key=UPLOAD_SPEED, - name="Up Speed", + name="Up speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, value=lambda data: get_state(data, UPLOAD_SPEED), @@ -92,7 +92,6 @@ class DelugeSensor(DelugeEntity, SensorEntity): """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.config_entry.title} {description.name}" self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" @property diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index cc11fcaf86c..c25c1752ee4 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -29,7 +29,6 @@ class DelugeSwitch(DelugeEntity, SwitchEntity): def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: """Initialize the Deluge switch.""" super().__init__(coordinator) - self._attr_name = coordinator.config_entry.title self._attr_unique_id = f"{coordinator.config_entry.entry_id}_enabled" def turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/deluge/translations/de.json b/homeassistant/components/deluge/translations/de.json index 9e8d559a523..4fa07b82d0f 100644 --- a/homeassistant/components/deluge/translations/de.json +++ b/homeassistant/components/deluge/translations/de.json @@ -16,7 +16,7 @@ "username": "Benutzername", "web_port": "Webport (f\u00fcr Besuchsdienste)" }, - "description": "Um diese Integration nutzen zu k\u00f6nnen, musst du die folgende Option in den Deluge-Einstellungen aktivieren: Daemon > Fernsteuerungen zulassen" + "description": "Um diese Integration nutzen zu k\u00f6nnen, musst du die folgende Option in den Deluge-Einstellungen aktivieren: Daemon \u2192 Fernsteuerungen zulassen" } } } diff --git a/homeassistant/components/deluge/translations/ja.json b/homeassistant/components/deluge/translations/ja.json index ab796327717..d109e1570c5 100644 --- a/homeassistant/components/deluge/translations/ja.json +++ b/homeassistant/components/deluge/translations/ja.json @@ -16,7 +16,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "web_port": "Web\u30dd\u30fc\u30c8\uff08\u8a2a\u554f\u30b5\u30fc\u30d3\u30b9\u7528\uff09" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5229\u7528\u3059\u308b\u305f\u3081\u306b\u306f\u3001deluge\u306e\u8a2d\u5b9a\u3067\u4ee5\u4e0b\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30fc\u30e2\u30f3 -> \u30ea\u30e2\u30fc\u30c8\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u306e\u8a31\u53ef" + "description": "\u3053\u306e\u7d71\u5408\u3092\u5229\u7528\u3059\u308b\u305f\u3081\u306b\u306f\u3001deluge\u306e\u8a2d\u5b9a\u3067\u4ee5\u4e0b\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30fc\u30e2\u30f3 -> \u30ea\u30e2\u30fc\u30c8\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u306e\u8a31\u53ef" } } } diff --git a/homeassistant/components/deluge/translations/pt.json b/homeassistant/components/deluge/translations/pt.json new file mode 100644 index 00000000000..fb1af357526 --- /dev/null +++ b/homeassistant/components/deluge/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index d6c5a5d3afc..3f0bb09cdbd 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -10,6 +10,7 @@ from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -177,6 +178,39 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen(EVENT_HOMEASSISTANT_START, demo_start_listener) + # Create issues + async_create_issue( + hass, + DOMAIN, + "transmogrifier_deprecated", + breaks_in_ha_version="2023.1.1", + is_fixable=False, + learn_more_url="https://en.wiktionary.org/wiki/transmogrifier", + severity=IssueSeverity.WARNING, + translation_key="transmogrifier_deprecated", + ) + + async_create_issue( + hass, + DOMAIN, + "out_of_blinker_fluid", + breaks_in_ha_version="2023.1.1", + is_fixable=True, + learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU", + severity=IssueSeverity.CRITICAL, + translation_key="out_of_blinker_fluid", + ) + + async_create_issue( + hass, + DOMAIN, + "unfixable_problem", + is_fixable=False, + learn_more_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", + severity=IssueSeverity.WARNING, + translation_key="unfixable_problem", + ) + return True @@ -259,10 +293,9 @@ async def _insert_statistics(hass): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set the config entry up.""" # Set up demo platforms with config entry - for platform in COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + await hass.config_entries.async_forward_entry_setups( + config_entry, COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM + ) if "recorder" in hass.config.components: await _insert_statistics(hass) return True diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index bf2731d3d72..f2b759a9cf3 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -73,7 +73,7 @@ class DemoBinarySensor(BinarySensorEntity): ) @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id.""" return self._unique_id @@ -83,16 +83,16 @@ class DemoBinarySensor(BinarySensorEntity): return self._sensor_type @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo binary sensor.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the binary sensor.""" return self._name @property - def is_on(self): + def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self._state diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 3a8b909bd0c..f8a803ba860 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -72,7 +72,12 @@ class DemoCalendar(CalendarEntity): """Return the name of the entity.""" return self._name - async def async_get_events(self, hass, start_date, end_date) -> list[CalendarEvent]: + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime.datetime, + end_date: datetime.datetime, + ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" return [self._event] @@ -80,15 +85,15 @@ class DemoCalendar(CalendarEntity): class LegacyDemoCalendar(CalendarEventDevice): """Calendar for exercising shim API.""" - def __init__(self, name): + def __init__(self, name: str) -> None: """Initialize demo calendar.""" self._name = name - one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30) + one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) self._event = { "start": {"dateTime": one_hour_from_now.isoformat()}, "end": { "dateTime": ( - one_hour_from_now + dt_util.dt.timedelta(minutes=60) + one_hour_from_now + datetime.timedelta(minutes=60) ).isoformat() }, "summary": "Future Event", @@ -102,7 +107,7 @@ class LegacyDemoCalendar(CalendarEventDevice): return self._event @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 25026bce11b..b8e0a714253 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -58,23 +58,23 @@ class DemoCamera(Camera): return await self.hass.async_add_executor_job(image_path.read_bytes) - async def async_enable_motion_detection(self): + async def async_enable_motion_detection(self) -> None: """Enable the Motion detection in base station (Arm).""" self._attr_motion_detection_enabled = True self.async_write_ha_state() - async def async_disable_motion_detection(self): + async def async_disable_motion_detection(self) -> None: """Disable the motion detection in base station (Disarm).""" self._attr_motion_detection_enabled = False self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off camera.""" self._attr_is_streaming = False self._attr_is_on = False self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on camera.""" self._attr_is_streaming = True self._attr_is_on = True diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index a4d6ca6da07..ae633c5937a 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,6 +1,8 @@ """Demo platform that offers a fake climate device.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, @@ -103,24 +105,24 @@ class DemoClimate(ClimateEntity): def __init__( self, - unique_id, - name, - target_temperature, - unit_of_measurement, - preset, - current_temperature, - fan_mode, - target_humidity, - current_humidity, - swing_mode, - hvac_mode, - hvac_action, - aux, - target_temp_high, - target_temp_low, - hvac_modes, - preset_modes=None, - ): + unique_id: str, + name: str, + target_temperature: float | None, + unit_of_measurement: str, + preset: str | None, + current_temperature: float, + fan_mode: str | None, + target_humidity: int | None, + current_humidity: int | None, + swing_mode: str | None, + hvac_mode: HVACMode, + hvac_action: HVACAction | None, + aux: bool | None, + target_temp_high: float | None, + target_temp_low: float | None, + hvac_modes: list[HVACMode], + preset_modes: list[str] | None = None, + ) -> None: """Initialize the climate device.""" self._unique_id = unique_id self._name = name @@ -175,111 +177,111 @@ class DemoClimate(ClimateEntity): ) @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id.""" return self._unique_id @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return self._support_flags @property - def should_poll(self): + def should_poll(self) -> bool: """Return the polling state.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the climate device.""" return self._name @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self._current_temperature @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._target_temperature @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" return self._target_temperature_high @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return self._target_temperature_low @property - def current_humidity(self): + def current_humidity(self) -> int | None: """Return the current humidity.""" return self._current_humidity @property - def target_humidity(self): + def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" return self._target_humidity @property - def hvac_action(self): + def hvac_action(self) -> HVACAction | None: """Return current operation ie. heat, cool, idle.""" return self._hvac_action @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode: """Return hvac target hvac state.""" return self._hvac_mode @property - def hvac_modes(self): + def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" return self._hvac_modes @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return preset mode.""" return self._preset @property - def preset_modes(self): + def preset_modes(self) -> list[str] | None: """Return preset modes.""" return self._preset_modes @property - def is_aux_heat(self): + def is_aux_heat(self) -> bool | None: """Return true if aux heat is on.""" return self._aux @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the fan setting.""" return self._current_fan_mode @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return self._fan_modes @property - def swing_mode(self): + def swing_mode(self) -> str | None: """Return the swing setting.""" return self._current_swing_mode @property - def swing_modes(self): + def swing_modes(self) -> list[str]: """List of available swing modes.""" return self._swing_modes - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if kwargs.get(ATTR_TEMPERATURE) is not None: self._target_temperature = kwargs.get(ATTR_TEMPERATURE) @@ -291,37 +293,37 @@ class DemoClimate(ClimateEntity): self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) self.async_write_ha_state() - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Set new humidity level.""" self._target_humidity = humidity self.async_write_ha_state() - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" self._current_swing_mode = swing_mode self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" self._current_fan_mode = fan_mode self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" self._hvac_mode = hvac_mode self.async_write_ha_state() - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Update preset_mode on.""" self._preset = preset_mode self.async_write_ha_state() - async def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" self._aux = True self.async_write_ha_state() - async def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" self._aux = False self.async_write_ha_state() diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index e389574c658..0163123b578 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import DOMAIN @@ -29,7 +30,7 @@ class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, import_info): + async def async_step_import(self, import_info) -> FlowResult: """Set the config entry up from yaml.""" return self.async_create_entry(title="Demo", data={}) @@ -42,11 +43,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.config_entry = config_entry self.options = dict(config_entry.options) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> FlowResult: """Manage the options.""" return await self.async_step_options_1() - async def async_step_options_1(self, user_input=None): + async def async_step_options_1(self, user_input=None) -> FlowResult: """Manage the options.""" if user_input is not None: self.options.update(user_input) @@ -69,7 +70,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def async_step_options_2(self, user_input=None): + async def async_step_options_2(self, user_input=None) -> FlowResult: """Manage the options 2.""" if user_input is not None: self.options.update(user_input) diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index fae626f37b2..27935300959 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -54,15 +54,15 @@ def setup_platform( class DemoManager: """Device manager for demo geolocation events.""" - def __init__(self, hass, add_entities): + def __init__(self, hass: HomeAssistant, add_entities: AddEntitiesCallback) -> None: """Initialise the demo geolocation event manager.""" self._hass = hass self._add_entities = add_entities - self._managed_devices = [] + self._managed_devices: list[DemoGeolocationEvent] = [] self._update(count=NUMBER_OF_DEMO_DEVICES) self._init_regular_updates() - def _generate_random_event(self): + def _generate_random_event(self) -> DemoGeolocationEvent: """Generate a random event in vicinity of this HA instance.""" home_latitude = self._hass.config.latitude home_longitude = self._hass.config.longitude @@ -83,13 +83,13 @@ class DemoManager: event_name, radius_in_km, latitude, longitude, LENGTH_KILOMETERS ) - def _init_regular_updates(self): + def _init_regular_updates(self) -> None: """Schedule regular updates based on configured time interval.""" track_time_interval( self._hass, lambda now: self._update(), DEFAULT_UPDATE_INTERVAL ) - def _update(self, count=1): + def _update(self, count: int = 1) -> None: """Remove events and add new random events.""" # Remove devices. for _ in range(1, count + 1): @@ -112,7 +112,14 @@ class DemoManager: class DemoGeolocationEvent(GeolocationEvent): """This represents a demo geolocation event.""" - def __init__(self, name, distance, latitude, longitude, unit_of_measurement): + def __init__( + self, + name: str, + distance: float, + latitude: float, + longitude: float, + unit_of_measurement: str, + ) -> None: """Initialize entity with data provided.""" self._name = name self._distance = distance @@ -131,7 +138,7 @@ class DemoGeolocationEvent(GeolocationEvent): return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo geolocation event.""" return False @@ -151,6 +158,6 @@ class DemoGeolocationEvent(GeolocationEvent): return self._longitude @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index c04c44cd8c5..c998a32ab55 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -1,6 +1,8 @@ """Demo platform that offers a fake humidifier device.""" from __future__ import annotations +from typing import Any + from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity from homeassistant.components.humidifier.const import HumidifierEntityFeature from homeassistant.config_entries import ConfigEntry @@ -78,22 +80,22 @@ class DemoHumidifier(HumidifierEntity): self._attr_available_modes = available_modes self._attr_device_class = device_class - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._attr_is_on = False self.async_write_ha_state() - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Set new humidity level.""" self._attr_target_humidity = humidity self.async_write_ha_state() - async def async_set_mode(self, mode): + async def async_set_mode(self, mode: str) -> None: """Update mode.""" self._attr_mode = mode self.async_write_ha_state() diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 070d8dcfa9c..58c884cc439 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,6 +1,7 @@ """Support for the demo image processing.""" from __future__ import annotations +from homeassistant.components.camera import Image from homeassistant.components.image_processing import ( ATTR_AGE, ATTR_CONFIDENCE, @@ -34,7 +35,7 @@ def setup_platform( class DemoImageProcessingAlpr(ImageProcessingAlprEntity): """Demo ALPR image processing entity.""" - def __init__(self, camera_entity, name): + def __init__(self, camera_entity: str, name: str) -> None: """Initialize demo ALPR image processing entity.""" super().__init__() @@ -42,21 +43,21 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): self._camera = camera_entity @property - def camera_entity(self): + def camera_entity(self) -> str: """Return camera entity id from process pictures.""" return self._camera @property - def confidence(self): + def confidence(self) -> int: """Return minimum confidence for send events.""" return 80 @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name - def process_image(self, image): + def process_image(self, image: Image) -> None: """Process image.""" demo_data = { "AC3829": 98.3, @@ -71,7 +72,7 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): class DemoImageProcessingFace(ImageProcessingFaceEntity): """Demo face identify image processing entity.""" - def __init__(self, camera_entity, name): + def __init__(self, camera_entity: str, name: str) -> None: """Initialize demo face image processing entity.""" super().__init__() @@ -79,21 +80,21 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity): self._camera = camera_entity @property - def camera_entity(self): + def camera_entity(self) -> str: """Return camera entity id from process pictures.""" return self._camera @property - def confidence(self): + def confidence(self) -> int: """Return minimum confidence for send events.""" return 80 @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name - def process_image(self, image): + def process_image(self, image: Image) -> None: """Process image.""" demo_data = [ { diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index a9fc6cf2044..00e5fd4def1 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import random +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -107,18 +108,18 @@ class DemoLight(LightEntity): def __init__( self, - unique_id, - name, + unique_id: str, + name: str, state, available=False, brightness=180, ct=None, # pylint: disable=invalid-name - effect_list=None, + effect_list: list[str] | None = None, effect=None, hs_color=None, rgbw_color=None, rgbww_color=None, - supported_color_modes=None, + supported_color_modes: set[ColorMode] | None = None, ): """Initialize the light.""" self._available = True @@ -169,7 +170,7 @@ class DemoLight(LightEntity): return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for light.""" return self._unique_id @@ -211,7 +212,7 @@ class DemoLight(LightEntity): return self._ct @property - def effect_list(self) -> list: + def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" return self._effect_list @@ -231,11 +232,11 @@ class DemoLight(LightEntity): return self._features @property - def supported_color_modes(self) -> set | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" return self._color_modes - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" self._state = True @@ -269,7 +270,7 @@ class DemoLight(LightEntity): # Home Assistant about updates in our state ourselves. self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._state = False diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index df6fa494079..2965a66e23e 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -3,7 +3,7 @@ "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", "after_dependencies": ["recorder"], - "dependencies": ["conversation", "group", "zone"], + "dependencies": ["conversation", "group", "repairs", "zone"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "calculated" diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 02ab1a2a989..17382ab9962 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -1,9 +1,9 @@ """Demo platform that offers a fake Number entity.""" from __future__ import annotations -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.const import DEVICE_DEFAULT_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -35,10 +35,10 @@ async def async_setup_platform( 0.42, "mdi:square-wave", False, - 0.0, - 1.0, - 0.01, - NumberMode.BOX, + native_min_value=0.0, + native_max_value=1.0, + native_step=0.01, + mode=NumberMode.BOX, ), DemoNumber( "large_range", @@ -46,9 +46,9 @@ async def async_setup_platform( 500, "mdi:square-wave", False, - 1, - 1000, - 1, + native_min_value=1, + native_max_value=1000, + native_step=1, ), DemoNumber( "small_range", @@ -56,9 +56,22 @@ async def async_setup_platform( 128, "mdi:square-wave", False, - 1, - 255, - 1, + native_min_value=1, + native_max_value=255, + native_step=1, + ), + DemoNumber( + "temp1", + "Temperature setting", + 22, + "mdi:thermometer", + False, + device_class=NumberDeviceClass.TEMPERATURE, + native_min_value=15.0, + native_max_value=35.0, + native_step=1, + mode=NumberMode.BOX, + unit_of_measurement=TEMP_CELSIUS, ), ] ) @@ -84,19 +97,24 @@ class DemoNumber(NumberEntity): name: str, state: float, icon: str, - assumed: bool, + assumed_state: bool, + *, + device_class: NumberDeviceClass | None = None, + mode: NumberMode = NumberMode.AUTO, native_min_value: float | None = None, native_max_value: float | None = None, native_step: float | None = None, - mode: NumberMode = NumberMode.AUTO, + unit_of_measurement: str | None = None, ) -> None: """Initialize the Demo Number entity.""" - self._attr_assumed_state = assumed + self._attr_assumed_state = assumed_state + self._attr_device_class = device_class self._attr_icon = icon - self._attr_name = name or DEVICE_DEFAULT_NAME - self._attr_unique_id = unique_id - self._attr_native_value = state self._attr_mode = mode + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_native_value = state + self._attr_unique_id = unique_id if native_min_value is not None: self._attr_native_min_value = native_min_value diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/demo/repairs.py new file mode 100644 index 00000000000..e5d31c18971 --- /dev/null +++ b/homeassistant/components/demo/repairs.py @@ -0,0 +1,33 @@ +"""Repairs platform for the demo integration.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow + + +class DemoFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await (self.async_step_confirm()) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + return self.async_create_entry(title="Fixed issue", data={}) + + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +async def async_create_fix_flow(hass, issue_id): + """Create flow.""" + return DemoFixFlow() diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index c861ca9e5e9..a3a5b11f336 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -1,5 +1,26 @@ { "title": "Demo", + "issues": { + "out_of_blinker_fluid": { + "title": "The blinker fluid is empty and needs to be refilled", + "fix_flow": { + "step": { + "confirm": { + "title": "Blinker fluid needs to be refilled", + "description": "Press OK when blinker fluid has been refilled" + } + } + } + }, + "transmogrifier_deprecated": { + "title": "The transmogrifier component is deprecated", + "description": "The transmogrifier component is now deprecated due to the lack of local control available in the new API" + }, + "unfixable_problem": { + "title": "This is not a fixable problem", + "description": "This issue is never going to give up." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 217119e9372..2ad400ff3f7 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,6 +1,8 @@ """Demo platform that has two fake switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME @@ -69,12 +71,12 @@ class DemoSwitch(SwitchEntity): name=self.name, ) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._attr_is_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index dbccaaf24a2..4ffda91d97c 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Prem D'acord quan s'hagi omplert el l\u00edquid d'intermitents", + "title": "Cal omplir el l\u00edquid d'intermitents" + } + } + }, + "title": "El l\u00edquid d'intermitents est\u00e0 buit i s'ha d'omplir" + }, + "transmogrifier_deprecated": { + "description": "El component 'transmogrifier' est\u00e0 obsolet, ja que el control local ja no est\u00e0 disponible a la nova API", + "title": "El component 'transmogrifier' est\u00e0 obsolet" + }, + "unfixable_problem": { + "description": "Aquest problema no es rendir\u00e0 mai.", + "title": "No \u00e9s un problema solucionable" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/de.json b/homeassistant/components/demo/translations/de.json index fd6239fa787..ab06043d52c 100644 --- a/homeassistant/components/demo/translations/de.json +++ b/homeassistant/components/demo/translations/de.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Dr\u00fccke OK, wenn die Blinkerfl\u00fcssigkeit nachgef\u00fcllt wurde.", + "title": "Blinkerfl\u00fcssigkeit muss nachgef\u00fcllt werden" + } + } + }, + "title": "Die Blinkerfl\u00fcssigkeit ist leer und muss nachgef\u00fcllt werden" + }, + "transmogrifier_deprecated": { + "description": "Die Transmogrifier-Komponente ist jetzt veraltet, da die neue API keine lokale Kontrolle mehr bietet.", + "title": "Die Transmogrifier-Komponente ist veraltet" + }, + "unfixable_problem": { + "description": "Dieses Problem wird niemals aufgeben.", + "title": "Dieses Problem kann nicht behoben werden" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/el.json b/homeassistant/components/demo/translations/el.json index 34afbe9df01..c4c539034bd 100644 --- a/homeassistant/components/demo/translations/el.json +++ b/homeassistant/components/demo/translations/el.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u03a0\u03b9\u03ad\u03c3\u03c4\u03b5 OK \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9.", + "title": "\u03a4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9" + } + } + }, + "title": "\u03a4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03b4\u03b5\u03b9\u03bf \u03ba\u03b1\u03b9 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9." + }, + "transmogrifier_deprecated": { + "description": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf transmogrifier \u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bb\u03cc\u03b3\u03c9 \u03c4\u03b7\u03c2 \u03ad\u03bb\u03bb\u03b5\u03b9\u03c8\u03b7\u03c2 \u03c4\u03bf\u03c0\u03b9\u03ba\u03bf\u03cd \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c0\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf\u03c2 \u03c3\u03c4\u03bf \u03bd\u03ad\u03bf API", + "title": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03c4\u03bf\u03c5 transmogrifier \u03b1\u03c0\u03bf\u03c3\u03cd\u03c1\u03b5\u03c4\u03b1\u03b9" + }, + "unfixable_problem": { + "description": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03c0\u03bf\u03c4\u03ad \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03bb\u03b5\u03af\u03c8\u03b5\u03b9.", + "title": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/en.json b/homeassistant/components/demo/translations/en.json index 2e70c88962a..11378fb94d4 100644 --- a/homeassistant/components/demo/translations/en.json +++ b/homeassistant/components/demo/translations/en.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Press OK when blinker fluid has been refilled", + "title": "Blinker fluid needs to be refilled" + } + } + }, + "title": "The blinker fluid is empty and needs to be refilled" + }, + "transmogrifier_deprecated": { + "description": "The transmogrifier component is now deprecated due to the lack of local control available in the new API", + "title": "The transmogrifier component is deprecated" + }, + "unfixable_problem": { + "description": "This issue is never going to give up.", + "title": "This is not a fixable problem" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/et.json b/homeassistant/components/demo/translations/et.json index ca06ed9c3bd..0e4c89dba01 100644 --- a/homeassistant/components/demo/translations/et.json +++ b/homeassistant/components/demo/translations/et.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Vajuta OK kui Blinkeri vedelik on uuesti t\u00e4idetud", + "title": "Blinkeri vedelikku on vaja uuesti t\u00e4ita" + } + } + }, + "title": "Blinkeri vedelik on otsas ja seda tuleb uuesti t\u00e4ita" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier komponent on n\u00fc\u00fcd aegunud, kuna uues API-s puudub kohalik kontroll", + "title": "Transmogrifieri komponent on aegunud" + }, + "unfixable_problem": { + "description": "See teema ei anna kunagi alla.", + "title": "See ei ole lahendatav probleem" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index 2f979d80a32..c5a150d8cb6 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Appuyez sur OK une fois le liquide de clignotant rempli", + "title": "Le liquide de clignotant doit \u00eatre rempli" + } + } + }, + "title": "Le r\u00e9servoir de liquide de clignotant est vide et doit \u00eatre rempli" + }, + "transmogrifier_deprecated": { + "description": "Le composant de transmogrification est d\u00e9sormais obsol\u00e8te en raison de l'absence de contr\u00f4le local dans la nouvelle API", + "title": "Le composant de transmogrification est obsol\u00e8te" + }, + "unfixable_problem": { + "description": "Ce probl\u00e8me ne va jamais s'arr\u00eater.", + "title": "Ce probl\u00e8me ne peut \u00eatre corrig\u00e9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index 87810814aac..1ff83e4ed06 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Nyomja meg az OK gombot, ha a villog\u00f3 folyad\u00e9kot felt\u00f6lt\u00f6tt\u00e9k.", + "title": "A villog\u00f3 folyad\u00e9kot fel kell t\u00f6lteni" + } + } + }, + "title": "A villog\u00f3 folyad\u00e9k ki\u00fcr\u00fclt, \u00e9s \u00fajra kell t\u00f6lteni" + }, + "transmogrifier_deprecated": { + "description": "A transzmogrifier komponens az \u00faj API-ban el\u00e9rhet\u0151 helyi vez\u00e9rl\u00e9s hi\u00e1nya miatt elavult", + "title": "A transzmogrifier komponens elavult" + }, + "unfixable_problem": { + "description": "Ez az eset soha nem fog le\u00e1llni.", + "title": "Ez a hiba nem jav\u00edthat\u00f3" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/id.json b/homeassistant/components/demo/translations/id.json index 8adbeb3e3c4..e8f827e2b86 100644 --- a/homeassistant/components/demo/translations/id.json +++ b/homeassistant/components/demo/translations/id.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tekan Oke saat cairan blinker telah diisi ulang", + "title": "Cairan blinker perlu diisi ulang" + } + } + }, + "title": "Cairan blinker kosong dan perlu diisi ulang" + }, + "transmogrifier_deprecated": { + "description": "Komponen transmogrifier tidak akan digunakan lagi karena tidak tersedianya kontrol lokal yang tersedia di API baru", + "title": "Komponen transmogrifier tidak akan digunakan lagi" + }, + "unfixable_problem": { + "description": "Masalah ini akan terus terjadi.", + "title": "Masalah ini tidak dapat diatasi." + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 1c94dea053f..80281a62703 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Premere OK quando il liquido delle frecce \u00e8 stato riempito", + "title": "Il liquido delle frecce deve essere rabboccato" + } + } + }, + "title": "Il liquido delle frecce \u00e8 vuoto e deve essere rabboccato" + }, + "transmogrifier_deprecated": { + "description": "Il componente transmogrifier \u00e8 ora deprecato a causa della mancanza di controllo locale disponibile nella nuova API", + "title": "Il componente transmogrifier \u00e8 deprecato" + }, + "unfixable_problem": { + "description": "Questo problema non si risolver\u00e0 mai.", + "title": "Questo non \u00e8 un problema risolvibile" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index e543a83bfe5..30467e3df5b 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u306e\u88dc\u5145\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001OK\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u3092\u88dc\u5145\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + } + } + }, + "title": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u304c\u7a7a\u306b\u306a\u3063\u305f\u306e\u3067\u3001\u88dc\u5145\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + }, + "transmogrifier_deprecated": { + "description": "\u65b0\u3057\u3044API\u3067\u5229\u7528\u3067\u304d\u308b\u30ed\u30fc\u30ab\u30eb\u5236\u5fa1\u304c\u306a\u3044\u305f\u3081\u3001transmogrifier\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306f\u3001\u975e\u63a8\u5968\u306b\u306a\u308a\u307e\u3057\u305f", + "title": "transmogrifier\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306f\u3001\u975e\u63a8\u5968\u306b\u306a\u308a\u307e\u3057\u305f" + }, + "unfixable_problem": { + "description": "\u3053\u306e\u554f\u984c\u306f\u6c7a\u3057\u3066\u3042\u304d\u3089\u3081\u308b\u3064\u3082\u308a\u306f\u3042\u308a\u307e\u305b\u3093\u3002", + "title": "\u3053\u308c\u306f\u3001\u4fee\u6b63\u53ef\u80fd\u306a\u554f\u984c\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/pl.json b/homeassistant/components/demo/translations/pl.json index bc9e6701c65..c57a1e4f619 100644 --- a/homeassistant/components/demo/translations/pl.json +++ b/homeassistant/components/demo/translations/pl.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Naci\u015bnij OK po uzupe\u0142nieniu p\u0142ynu \u015bwiate\u0142ka", + "title": "P\u0142yn \u015bwiate\u0142ka nale\u017cy uzupe\u0142ni\u0107" + } + } + }, + "title": "P\u0142yn \u015bwiate\u0142ka jest pusty i nale\u017cy go uzupe\u0142ni\u0107" + }, + "transmogrifier_deprecated": { + "description": "Komponent transmogryfikatora jest ju\u017c przestarza\u0142y z powodu braku lokalnej kontroli w nowym API", + "title": "Komponent transmogryfikatora jest przestarza\u0142y" + }, + "unfixable_problem": { + "description": "Ten problem nigdy si\u0119 nie podda.", + "title": "Problem jest nie do naprawienia" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/pt-BR.json b/homeassistant/components/demo/translations/pt-BR.json index 49290be4ceb..26e903f345d 100644 --- a/homeassistant/components/demo/translations/pt-BR.json +++ b/homeassistant/components/demo/translations/pt-BR.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pressione OK quando o fluido do pisca-pisca for reabastecido", + "title": "O fluido do pisca-pisca precisa ser reabastecido" + } + } + }, + "title": "O fluido do pisca-pisca est\u00e1 vazio e precisa ser reabastecido" + }, + "transmogrifier_deprecated": { + "description": "O componente transmogriifier agora est\u00e1 obsoleto devido \u00e0 falta de controle local dispon\u00edvel na nova API", + "title": "O componente transmogriificador est\u00e1 obsoleto" + }, + "unfixable_problem": { + "description": "Esta quest\u00e3o nunca vai desistir.", + "title": "Este n\u00e3o \u00e9 um problema corrig\u00edvel" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/pt.json b/homeassistant/components/demo/translations/pt.json index db34017d6e2..7d9ee992b39 100644 --- a/homeassistant/components/demo/translations/pt.json +++ b/homeassistant/components/demo/translations/pt.json @@ -3,6 +3,7 @@ "step": { "options_1": { "data": { + "bool": "Booleano opcional", "constant": "Constante" } } diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index e3bd96b880a..3c919e12d84 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 OK, \u043a\u043e\u0433\u0434\u0430 \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430.", + "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u043b\u0438\u0442\u044c \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432" + } + } + }, + "title": "\u0416\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0437\u0430\u043a\u0430\u043d\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f" + }, + "transmogrifier_deprecated": { + "description": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0442\u0440\u0430\u043d\u0441\u043c\u043e\u0433\u0440\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u043b \u0432 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435\u043c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0432 \u043d\u043e\u0432\u043e\u043c API.", + "title": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0442\u0440\u0430\u043d\u0441\u043c\u043e\u0433\u0440\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u043b" + }, + "unfixable_problem": { + "description": "\u042d\u0442\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u043e\u0442\u0441\u0442\u0443\u043f\u0438\u0442.", + "title": "\u042d\u0442\u043e \u043d\u0435 \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/select.pt.json b/homeassistant/components/demo/translations/select.pt.json new file mode 100644 index 00000000000..438f02fa47f --- /dev/null +++ b/homeassistant/components/demo/translations/select.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Velocidade da Luz" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json index 1eea23e1bc5..b7ff98f9cd4 100644 --- a/homeassistant/components/demo/translations/tr.json +++ b/homeassistant/components/demo/translations/tr.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Fla\u015f\u00f6r yeniden dolduruldu\u011funda Tamam'a bas\u0131n", + "title": "Fla\u015f\u00f6r\u00fcn yeniden doldurulmas\u0131 gerekiyor" + } + } + }, + "title": "Fla\u015f\u00f6r bo\u015f ve yeniden doldurulmas\u0131 gerekiyor" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier bile\u015feni, yeni API'de mevcut olan yerel kontrol eksikli\u011fi nedeniyle art\u0131k kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015ft\u0131r", + "title": "Transmogrifier bile\u015feni kullan\u0131mdan kald\u0131r\u0131ld\u0131" + }, + "unfixable_problem": { + "description": "Bu konudan asla vazge\u00e7ilmeyecek.", + "title": "Bu d\u00fczeltilebilir bir sorun de\u011fil" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/zh-Hant.json b/homeassistant/components/demo/translations/zh-Hant.json index f9f798134ba..a984c9c62ca 100644 --- a/homeassistant/components/demo/translations/zh-Hant.json +++ b/homeassistant/components/demo/translations/zh-Hant.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u65bc\u8a0a\u865f\u5291\u88dc\u5145\u5f8c\u6309\u4e0b OK", + "title": "\u8a0a\u865f\u5291\u9700\u8981\u88dc\u5145" + } + } + }, + "title": "\u8a0a\u865f\u5291\u5df2\u7528\u5b8c\u3001\u9700\u8981\u91cd\u65b0\u88dc\u5145" + }, + "transmogrifier_deprecated": { + "description": "\u7531\u65bc\u65b0 API \u7f3a\u4e4f\u672c\u5730\u7aef\u63a7\u5236\u652f\u63f4\u3001Transmogrifier \u5143\u4ef6\u5df2\u7d93\u4e0d\u63a8\u85a6\u4f7f\u7528", + "title": "Transmogrifier \u5143\u4ef6\u5df2\u7d93\u4e0d\u63a8\u85a6\u4f7f\u7528" + }, + "unfixable_problem": { + "description": "\u9019\u554f\u984c\u7e3d\u662f\u4e0d\u6b7b\u5fc3\u7684\u51fa\u73fe\u3002", + "title": "\u9019\u4e0d\u662f\u4e00\u500b\u53ef\u4fee\u7684\u554f\u984c" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index feda379558b..58b76ba6347 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -1,6 +1,9 @@ """Demo platform for the vacuum component.""" from __future__ import annotations +from datetime import datetime +from typing import Any + from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, STATE_CLEANING, @@ -101,62 +104,62 @@ async def async_setup_platform( class DemoVacuum(VacuumEntity): """Representation of a demo vacuum.""" - def __init__(self, name, supported_features): + def __init__(self, name: str, supported_features: int) -> None: """Initialize the vacuum.""" self._name = name self._supported_features = supported_features self._state = False self._status = "Charging" self._fan_speed = FAN_SPEEDS[1] - self._cleaned_area = 0 + self._cleaned_area: float = 0 self._battery_level = 100 @property - def name(self): + def name(self) -> str: """Return the name of the vacuum.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo vacuum.""" return False @property - def is_on(self): + def is_on(self) -> bool: """Return true if vacuum is on.""" return self._state @property - def status(self): + def status(self) -> str: """Return the status of the vacuum.""" return self._status @property - def fan_speed(self): + def fan_speed(self) -> str: """Return the status of the vacuum.""" return self._fan_speed @property - def fan_speed_list(self): + def fan_speed_list(self) -> list[str]: """Return the status of the vacuum.""" return FAN_SPEEDS @property - def battery_level(self): + def battery_level(self) -> int: """Return the status of the vacuum.""" return max(0, min(100, self._battery_level)) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return self._supported_features - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the vacuum on.""" if self.supported_features & VacuumEntityFeature.TURN_ON == 0: return @@ -167,7 +170,7 @@ class DemoVacuum(VacuumEntity): self._status = "Cleaning" self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the vacuum off.""" if self.supported_features & VacuumEntityFeature.TURN_OFF == 0: return @@ -176,7 +179,7 @@ class DemoVacuum(VacuumEntity): self._status = "Charging" self.schedule_update_ha_state() - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the vacuum.""" if self.supported_features & VacuumEntityFeature.STOP == 0: return @@ -185,7 +188,7 @@ class DemoVacuum(VacuumEntity): self._status = "Stopping the current task" self.schedule_update_ha_state() - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" if self.supported_features & VacuumEntityFeature.CLEAN_SPOT == 0: return @@ -196,7 +199,7 @@ class DemoVacuum(VacuumEntity): self._status = "Cleaning spot" self.schedule_update_ha_state() - def locate(self, **kwargs): + def locate(self, **kwargs: Any) -> None: """Locate the vacuum (usually by playing a song).""" if self.supported_features & VacuumEntityFeature.LOCATE == 0: return @@ -204,7 +207,7 @@ class DemoVacuum(VacuumEntity): self._status = "Hi, I'm over here!" self.schedule_update_ha_state() - def start_pause(self, **kwargs): + def start_pause(self, **kwargs: Any) -> None: """Start, pause or resume the cleaning task.""" if self.supported_features & VacuumEntityFeature.PAUSE == 0: return @@ -218,7 +221,7 @@ class DemoVacuum(VacuumEntity): self._status = "Pausing the current task" self.schedule_update_ha_state() - def set_fan_speed(self, fan_speed, **kwargs): + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the vacuum's fan speed.""" if self.supported_features & VacuumEntityFeature.FAN_SPEED == 0: return @@ -227,7 +230,7 @@ class DemoVacuum(VacuumEntity): self._fan_speed = fan_speed self.schedule_update_ha_state() - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Tell the vacuum to return to its dock.""" if self.supported_features & VacuumEntityFeature.RETURN_HOME == 0: return @@ -237,7 +240,7 @@ class DemoVacuum(VacuumEntity): self._battery_level += 5 self.schedule_update_ha_state() - def send_command(self, command, params=None, **kwargs): + def send_command(self, command, params=None, **kwargs: Any) -> None: """Send a command to the vacuum.""" if self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0: return @@ -250,56 +253,56 @@ class DemoVacuum(VacuumEntity): class StateDemoVacuum(StateVacuumEntity): """Representation of a demo vacuum supporting states.""" - def __init__(self, name): + def __init__(self, name: str) -> None: """Initialize the vacuum.""" self._name = name self._supported_features = SUPPORT_STATE_SERVICES self._state = STATE_DOCKED self._fan_speed = FAN_SPEEDS[1] - self._cleaned_area = 0 + self._cleaned_area: float = 0 self._battery_level = 100 @property - def name(self): + def name(self) -> str: """Return the name of the vacuum.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo vacuum.""" return False @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return self._supported_features @property - def state(self): + def state(self) -> str: """Return the current state of the vacuum.""" return self._state @property - def battery_level(self): + def battery_level(self) -> int: """Return the current battery level of the vacuum.""" return max(0, min(100, self._battery_level)) @property - def fan_speed(self): + def fan_speed(self) -> str: """Return the current fan speed of the vacuum.""" return self._fan_speed @property - def fan_speed_list(self): + def fan_speed_list(self) -> list[str]: """Return the list of supported fan speeds.""" return FAN_SPEEDS @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} - def start(self): + def start(self) -> None: """Start or resume the cleaning task.""" if self.supported_features & VacuumEntityFeature.START == 0: return @@ -310,7 +313,7 @@ class StateDemoVacuum(StateVacuumEntity): self._battery_level -= 1 self.schedule_update_ha_state() - def pause(self): + def pause(self) -> None: """Pause the cleaning task.""" if self.supported_features & VacuumEntityFeature.PAUSE == 0: return @@ -319,7 +322,7 @@ class StateDemoVacuum(StateVacuumEntity): self._state = STATE_PAUSED self.schedule_update_ha_state() - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the cleaning task, do not return to dock.""" if self.supported_features & VacuumEntityFeature.STOP == 0: return @@ -327,7 +330,7 @@ class StateDemoVacuum(StateVacuumEntity): self._state = STATE_IDLE self.schedule_update_ha_state() - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Return dock to charging base.""" if self.supported_features & VacuumEntityFeature.RETURN_HOME == 0: return @@ -337,7 +340,7 @@ class StateDemoVacuum(StateVacuumEntity): event.call_later(self.hass, 30, self.__set_state_to_dock) - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" if self.supported_features & VacuumEntityFeature.CLEAN_SPOT == 0: return @@ -347,7 +350,7 @@ class StateDemoVacuum(StateVacuumEntity): self._battery_level -= 1 self.schedule_update_ha_state() - def set_fan_speed(self, fan_speed, **kwargs): + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the vacuum's fan speed.""" if self.supported_features & VacuumEntityFeature.FAN_SPEED == 0: return @@ -356,6 +359,6 @@ class StateDemoVacuum(StateVacuumEntity): self._fan_speed = fan_speed self.schedule_update_ha_state() - def __set_state_to_dock(self, _): + def __set_state_to_dock(self, _: datetime) -> None: self._state = STATE_DOCKED self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index eed3e970b12..dabaf8d066c 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -18,12 +18,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -40,7 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLOUDY: [], ATTR_CONDITION_FOG: [], ATTR_CONDITION_HAIL: [], @@ -125,17 +120,17 @@ class DemoWeather(WeatherEntity): def __init__( self, - name, - condition, - temperature, - humidity, - pressure, - wind_speed, - temperature_unit, - pressure_unit, - wind_speed_unit, - forecast, - ): + name: str, + condition: str, + temperature: float, + humidity: float, + pressure: float, + wind_speed: float, + temperature_unit: str, + pressure_unit: str, + wind_speed_unit: str, + forecast: list[list], + ) -> None: """Initialize the Demo weather.""" self._name = name self._condition = condition @@ -149,77 +144,77 @@ class DemoWeather(WeatherEntity): self._forecast = forecast @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"Demo Weather {self._name}" @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo weather condition.""" return False @property - def native_temperature(self): + def native_temperature(self) -> float: """Return the temperature.""" return self._native_temperature @property - def native_temperature_unit(self): + def native_temperature_unit(self) -> str: """Return the unit of measurement.""" return self._native_temperature_unit @property - def humidity(self): + def humidity(self) -> float: """Return the humidity.""" return self._humidity @property - def native_wind_speed(self): + def native_wind_speed(self) -> float: """Return the wind speed.""" return self._native_wind_speed @property - def native_wind_speed_unit(self): + def native_wind_speed_unit(self) -> str: """Return the wind speed.""" return self._native_wind_speed_unit @property - def native_pressure(self): + def native_pressure(self) -> float: """Return the pressure.""" return self._native_pressure @property - def native_pressure_unit(self): + def native_pressure_unit(self) -> str: """Return the pressure.""" return self._native_pressure_unit @property - def condition(self): + def condition(self) -> str: """Return the weather condition.""" return [ k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v ][0] @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return "Powered by Home Assistant" @property - def forecast(self): + def forecast(self) -> list[Forecast]: """Return the forecast.""" reftime = dt_util.now().replace(hour=16, minute=00) forecast_data = [] for entry in self._forecast: - data_dict = { - ATTR_FORECAST_TIME: reftime.isoformat(), - ATTR_FORECAST_CONDITION: entry[0], - ATTR_FORECAST_PRECIPITATION: entry[1], - ATTR_FORECAST_TEMP: entry[2], - ATTR_FORECAST_TEMP_LOW: entry[3], - ATTR_FORECAST_PRECIPITATION_PROBABILITY: entry[4], - } + data_dict = Forecast( + datetime=reftime.isoformat(), + condition=entry[0], + precipitation=entry[1], + temperature=entry[2], + templow=entry[3], + precipitation_probability=entry[4], + ) reftime = reftime + timedelta(hours=4) forecast_data.append(data_dict) diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 27594703fd2..0f58f5f5218 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index fe6c05b3aca..2ec58df8126 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -90,14 +90,14 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Denon AVR flow.""" - self.host = None - self.serial_number = None - self.model_name = None + self.host: str | None = None + self.serial_number: str | None = None + self.model_name: str | None = None self.timeout = DEFAULT_TIMEOUT self.show_all_sources = DEFAULT_SHOW_SOURCES self.zone2 = DEFAULT_ZONE2 self.zone3 = DEFAULT_ZONE3 - self.d_receivers = [] + self.d_receivers: list[dict[str, Any]] = [] @staticmethod @callback @@ -138,7 +138,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle multiple receivers found.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: self.host = user_input["select_host"] return await self.async_step_connect() @@ -169,6 +169,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Connect to the receiver.""" + assert self.host connect_denonavr = ConnectDenonAVR( self.host, self.timeout, @@ -185,6 +186,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not success: return self.async_abort(reason="cannot_connect") receiver = connect_denonavr.receiver + assert receiver if not self.serial_number: self.serial_number = receiver.serial_number @@ -238,6 +240,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "*", "" ) self.serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + assert discovery_info.ssdp_location is not None self.host = urlparse(discovery_info.ssdp_location).hostname if self.model_name in IGNORED_MODELS: @@ -260,6 +263,6 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() @staticmethod - def construct_unique_id(model_name: str, serial_number: str) -> str: + def construct_unique_id(model_name: str | None, serial_number: str | None) -> str: """Construct the unique id from the ssdp discovery or user_step.""" return f"{model_name}-{serial_number}" diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 85e28c29d7c..16814b72bc7 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -1,10 +1,11 @@ """Support for Denon AVR receivers using their HTTP interface.""" from __future__ import annotations -from collections.abc import Coroutine +from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta from functools import wraps import logging +from typing import Any, TypeVar from denonavr import DenonAVR from denonavr.const import POWER_ON @@ -15,6 +16,7 @@ from denonavr.exceptions import ( AvrTimoutError, DenonAvrError, ) +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components.media_player import ( @@ -79,6 +81,10 @@ SERVICE_GET_COMMAND = "get_command" SERVICE_SET_DYNAMIC_EQ = "set_dynamic_eq" SERVICE_UPDATE_AUDYSSEY = "update_audyssey" +_DenonDeviceT = TypeVar("_DenonDeviceT", bound="DenonDevice") +_R = TypeVar("_R") +_P = ParamSpec("_P") + async def async_setup_entry( hass: HomeAssistant, @@ -131,6 +137,79 @@ async def async_setup_entry( async_add_entities(entities, update_before_add=True) +def async_log_errors( + func: Callable[Concatenate[_DenonDeviceT, _P], Awaitable[_R]], +) -> Callable[Concatenate[_DenonDeviceT, _P], Coroutine[Any, Any, _R | None]]: + """ + Log errors occurred when calling a Denon AVR receiver. + + Decorates methods of DenonDevice class. + Declaration of staticmethod for this method is at the end of this class. + """ + + @wraps(func) + async def wrapper( + self: _DenonDeviceT, *args: _P.args, **kwargs: _P.kwargs + ) -> _R | None: + # pylint: disable=protected-access + available = True + try: + return await func(self, *args, **kwargs) + except AvrTimoutError: + available = False + if self._available is True: + _LOGGER.warning( + "Timeout connecting to Denon AVR receiver at host %s. " + "Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrNetworkError: + available = False + if self._available is True: + _LOGGER.warning( + "Network error connecting to Denon AVR receiver at host %s. " + "Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrForbiddenError: + available = False + if self._available is True: + _LOGGER.warning( + "Denon AVR receiver at host %s responded with HTTP 403 error. " + "Device is unavailable. Please consider power cycling your " + "receiver", + self._receiver.host, + ) + self._available = False + except AvrCommandError as err: + available = False + _LOGGER.error( + "Command %s failed with error: %s", + func.__name__, + err, + ) + except DenonAvrError as err: + available = False + _LOGGER.error( + "Error %s occurred in method %s for Denon AVR receiver", + err, + func.__name__, + exc_info=True, + ) + finally: + if available is True and self._available is False: + _LOGGER.info( + "Denon AVR receiver at host %s is available again", + self._receiver.host, + ) + self._available = True + return None + + return wrapper + + class DenonDevice(MediaPlayerEntity): """Representation of a Denon Media Player Device.""" @@ -144,11 +223,13 @@ class DenonDevice(MediaPlayerEntity): """Initialize the device.""" self._attr_name = receiver.name self._attr_unique_id = unique_id + assert config_entry.unique_id self._attr_device_info = DeviceInfo( configuration_url=f"http://{config_entry.data[CONF_HOST]}/", + hw_version=config_entry.data[CONF_TYPE], identifiers={(DOMAIN, config_entry.unique_id)}, manufacturer=config_entry.data[CONF_MANUFACTURER], - model=f"{config_entry.data[CONF_MODEL]}-{config_entry.data[CONF_TYPE]}", + model=config_entry.data[CONF_MODEL], name=config_entry.title, ) self._attr_sound_mode_list = receiver.sound_mode_list @@ -163,71 +244,6 @@ class DenonDevice(MediaPlayerEntity): ) self._available = True - def async_log_errors( - func: Coroutine, - ) -> Coroutine: - """ - Log errors occurred when calling a Denon AVR receiver. - - Decorates methods of DenonDevice class. - Declaration of staticmethod for this method is at the end of this class. - """ - - @wraps(func) - async def wrapper(self, *args, **kwargs): - # pylint: disable=protected-access - available = True - try: - return await func(self, *args, **kwargs) - except AvrTimoutError: - available = False - if self._available is True: - _LOGGER.warning( - "Timeout connecting to Denon AVR receiver at host %s. Device is unavailable", - self._receiver.host, - ) - self._available = False - except AvrNetworkError: - available = False - if self._available is True: - _LOGGER.warning( - "Network error connecting to Denon AVR receiver at host %s. Device is unavailable", - self._receiver.host, - ) - self._available = False - except AvrForbiddenError: - available = False - if self._available is True: - _LOGGER.warning( - "Denon AVR receiver at host %s responded with HTTP 403 error. Device is unavailable. Please consider power cycling your receiver", - self._receiver.host, - ) - self._available = False - except AvrCommandError as err: - available = False - _LOGGER.error( - "Command %s failed with error: %s", - func.__name__, - err, - ) - except DenonAvrError as err: - available = False - _LOGGER.error( - "Error %s occurred in method %s for Denon AVR receiver", - err, - func.__name__, - exc_info=True, - ) - finally: - if available is True and self._available is False: - _LOGGER.info( - "Denon AVR receiver at host %s is available again", - self._receiver.host, - ) - self._available = True - - return wrapper - @async_log_errors async def async_update(self) -> None: """Get the latest status information from device.""" @@ -466,8 +482,3 @@ class DenonDevice(MediaPlayerEntity): if self._update_audyssey: await self._receiver.async_update_audyssey() - - # Decorator defined before is a staticmethod - async_log_errors = staticmethod( # pylint: disable=no-staticmethod-decorator - async_log_errors - ) diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index 5c15468e6d4..28969d25792 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -23,12 +23,12 @@ class ConnectDenonAVR: ) -> None: """Initialize the class.""" self._async_client_getter = async_client_getter - self._receiver = None + self._receiver: DenonAVR | None = None self._host = host self._show_all_inputs = show_all_inputs self._timeout = timeout - self._zones = {} + self._zones: dict[str, str | None] = {} if zone2: self._zones["Zone2"] = None if zone3: @@ -42,6 +42,7 @@ class ConnectDenonAVR: async def async_connect_receiver(self) -> bool: """Connect to the DenonAVR receiver.""" await self.async_init_receiver_class() + assert self._receiver if ( self._receiver.manufacturer is None @@ -70,7 +71,7 @@ class ConnectDenonAVR: return True - async def async_init_receiver_class(self) -> bool: + async def async_init_receiver_class(self) -> None: """Initialize the DenonAVR class asynchronously.""" receiver = DenonAVR( host=self._host, diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index 8c51c7e990f..302a485395f 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", - "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet", + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rem, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet", "not_denonavr_manufacturer": "Nem egy Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedezett gy\u00e1rt\u00f3n\u00e9v nem megfelel\u0151", "not_denonavr_missing": "Nem Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedez\u00e9si inform\u00e1ci\u00f3k nem teljesek" }, @@ -13,7 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "K\u00e9rj\u00fck, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t" + "description": "K\u00e9rem, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t" }, "select": { "data": { diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index e3fe9d85f41..c5b1c8e31e9 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Derivative from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/derivative/translations/pt.json b/homeassistant/components/derivative/translations/pt.json new file mode 100644 index 00000000000..6801ab6b6d4 --- /dev/null +++ b/homeassistant/components/derivative/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "round": "Precis\u00e3o" + } + } + } + }, + "options": { + "step": { + "init": { + "data_description": { + "unit_prefix": "." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/ru.json b/homeassistant/components/derivative/translations/ru.json index 6155d64301a..bef5b20efdd 100644 --- a/homeassistant/components/derivative/translations/ru.json +++ b/homeassistant/components/derivative/translations/ru.json @@ -12,7 +12,7 @@ }, "data_description": { "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", - "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", + "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439." }, "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0443\u044e \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", @@ -33,7 +33,7 @@ }, "data_description": { "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", - "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", + "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439.." } } diff --git a/homeassistant/components/derivative/translations/sv.json b/homeassistant/components/derivative/translations/sv.json index 66dcd34b1d7..ac5b766d124 100644 --- a/homeassistant/components/derivative/translations/sv.json +++ b/homeassistant/components/derivative/translations/sv.json @@ -1,12 +1,37 @@ { + "config": { + "step": { + "user": { + "data": { + "name": "Namn", + "round": "Precision", + "time_window": "Tidsf\u00f6nster", + "unit_prefix": "Metriskt prefix", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Anger antal decimaler i resultatet." + }, + "description": "Skapa en sensor som ber\u00e4knar derivatan av en sensor", + "title": "L\u00e4gg till derivatasensor" + } + } + }, "options": { "step": { "init": { "data": { "name": "Namn", - "round": "Precision" + "round": "Precision", + "time_window": "Tidsf\u00f6nster", + "unit_prefix": "Metriskt prefix", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Anger antal decimaler i resultatet." } } } - } + }, + "title": "Derivatasensor" } \ No newline at end of file diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 0a1ec495e70..93119d1b4a0 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable, Mapping +from collections.abc import Awaitable, Callable, Coroutine, Iterable, Mapping from enum import Enum from functools import wraps import logging @@ -13,6 +13,7 @@ import voluptuous as vol import voluptuous_serialize from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -43,10 +44,9 @@ if TYPE_CHECKING: DeviceAutomationActionProtocol, ] -# mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = "device_automation" -DEVICE_TRIGGER_BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( +DEVICE_TRIGGER_BASE_SCHEMA: vol.Schema = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str, @@ -310,11 +310,17 @@ async def _async_get_device_automation_capabilities( return capabilities # type: ignore[no-any-return] -def handle_device_errors(func): +def handle_device_errors( + func: Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Awaitable[None]] +) -> Callable[ + [HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None] +]: """Handle device automation errors.""" @wraps(func) - async def with_error_handling(hass, connection, msg): + async def with_error_handling( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] + ) -> None: try: await func(hass, connection, msg) except DeviceNotFound: @@ -333,7 +339,9 @@ def handle_device_errors(func): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_list_actions(hass, connection, msg): +async def websocket_device_automation_list_actions( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device actions.""" device_id = msg["device_id"] actions = ( @@ -352,7 +360,9 @@ async def websocket_device_automation_list_actions(hass, connection, msg): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_list_conditions(hass, connection, msg): +async def websocket_device_automation_list_conditions( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device conditions.""" device_id = msg["device_id"] conditions = ( @@ -371,7 +381,9 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_list_triggers(hass, connection, msg): +async def websocket_device_automation_list_triggers( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device triggers.""" device_id = msg["device_id"] triggers = ( @@ -390,7 +402,9 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_get_action_capabilities(hass, connection, msg): +async def websocket_device_automation_get_action_capabilities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device action capabilities.""" action = msg["action"] capabilities = await _async_get_device_automation_capabilities( @@ -409,7 +423,9 @@ async def websocket_device_automation_get_action_capabilities(hass, connection, ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_get_condition_capabilities(hass, connection, msg): +async def websocket_device_automation_get_condition_capabilities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device condition capabilities.""" condition = msg["condition"] capabilities = await _async_get_device_automation_capabilities( @@ -428,7 +444,9 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg): +async def websocket_device_automation_get_trigger_capabilities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device trigger capabilities.""" trigger = msg["trigger"] capabilities = await _async_get_device_automation_capabilities( diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 24075ee1a7d..e9222156b00 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -47,6 +47,14 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the device tracker.""" + + # We need to add the component here break the deadlock + # when setting up integrations from config entries as + # they would otherwise wait for the device tracker to be + # setup and thus the config entries would not be able to + # setup their platforms. + hass.config.components.add(DOMAIN) + await async_setup_legacy_integration(hass, config) return True diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index b79d0a5c8fe..ec0d5a3a666 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except GatewayOfflineError as err: raise ConfigEntryNotReady from err - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def shutdown(event: Event) -> None: for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: diff --git a/homeassistant/components/devolo_home_control/translations/hu.json b/homeassistant/components/devolo_home_control/translations/hu.json index 391eeb60727..b7fabbbef2f 100644 --- a/homeassistant/components/devolo_home_control/translations/hu.json +++ b/homeassistant/components/devolo_home_control/translations/hu.json @@ -6,7 +6,7 @@ }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "reauth_failed": "K\u00e9rj\u00fck, ugyanazt a mydevolo felhaszn\u00e1l\u00f3t haszn\u00e1lja, mint kor\u00e1bban." + "reauth_failed": "K\u00e9rem, ugyanazt a mydevolo felhaszn\u00e1l\u00f3t haszn\u00e1lja, mint kor\u00e1bban." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/pt.json b/homeassistant/components/devolo_home_control/translations/pt.json index 2215d148b7b..d60cc81f541 100644 --- a/homeassistant/components/devolo_home_control/translations/pt.json +++ b/homeassistant/components/devolo_home_control/translations/pt.json @@ -13,6 +13,11 @@ "password": "Palavra-passe", "username": "Email / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "password": "Palavra-passe" + } } } } diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index f427e5acbfc..5cf91325d70 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for coordinator in coordinators.values(): await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index 2dfdd3c1d9a..1a49beb5d02 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -5,8 +5,9 @@ from datetime import timedelta from homeassistant.const import Platform DOMAIN = "devolo_home_network" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] +MAC_ADDRESS = "mac_address" PRODUCT = "product" SERIAL_NUMBER = "serial_number" TITLE = "title" @@ -15,6 +16,16 @@ LONG_UPDATE_INTERVAL = timedelta(minutes=5) SHORT_UPDATE_INTERVAL = timedelta(seconds=15) CONNECTED_PLC_DEVICES = "connected_plc_devices" +CONNECTED_STATIONS = "connected_stations" CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" + +WIFI_APTYPE = { + "WIFI_VAP_MAIN_AP": "Main", + "WIFI_VAP_GUEST_AP": "Guest", +} +WIFI_BANDS = { + "WIFI_BAND_2G": 2.4, + "WIFI_BAND_5G": 5, +} diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py new file mode 100644 index 00000000000..9dffeef7db9 --- /dev/null +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -0,0 +1,159 @@ +"""Platform for device tracker integration.""" +from __future__ import annotations + +from typing import Any + +from devolo_plc_api.device import Device + +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, + SOURCE_TYPE_ROUTER, +) +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import FREQUENCY_GIGAHERTZ, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + CONNECTED_STATIONS, + CONNECTED_WIFI_CLIENTS, + DOMAIN, + MAC_ADDRESS, + WIFI_APTYPE, + WIFI_BANDS, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id][ + "coordinators" + ] + registry = entity_registry.async_get(hass) + tracked = set() + + @callback + def new_device_callback() -> None: + """Add new devices if needed.""" + new_entities = [] + for station in coordinators[CONNECTED_WIFI_CLIENTS].data[CONNECTED_STATIONS]: + if station[MAC_ADDRESS] in tracked: + continue + + new_entities.append( + DevoloScannerEntity( + coordinators[CONNECTED_WIFI_CLIENTS], device, station[MAC_ADDRESS] + ) + ) + tracked.add(station[MAC_ADDRESS]) + if new_entities: + async_add_entities(new_entities) + + @callback + def restore_entities() -> None: + """Restore clients that are not a part of active clients list.""" + missing = [] + for entity in entity_registry.async_entries_for_config_entry( + registry, entry.entry_id + ): + if ( + entity.platform == DOMAIN + and entity.domain == DEVICE_TRACKER_DOMAIN + and ( + mac_address := entity.unique_id.replace( + f"{device.serial_number}_", "" + ) + ) + not in tracked + ): + missing.append( + DevoloScannerEntity( + coordinators[CONNECTED_WIFI_CLIENTS], device, mac_address + ) + ) + tracked.add(mac_address) + + if missing: + async_add_entities(missing) + + if device.device and "wifi1" in device.device.features: + restore_entities() + entry.async_on_unload( + coordinators[CONNECTED_WIFI_CLIENTS].async_add_listener(new_device_callback) + ) + + +class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): + """Representation of a devolo device tracker.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, device: Device, mac: str + ) -> None: + """Initialize entity.""" + super().__init__(coordinator) + self._device = device + self._mac = mac + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the attributes.""" + attrs: dict[str, str] = {} + if not self.coordinator.data[CONNECTED_STATIONS]: + return {} + + station: dict[str, Any] = next( + ( + station + for station in self.coordinator.data[CONNECTED_STATIONS] + if station[MAC_ADDRESS] == self.mac_address + ), + {}, + ) + if station: + attrs["wifi"] = WIFI_APTYPE.get(station["vap_type"], STATE_UNKNOWN) + attrs["band"] = ( + f"{WIFI_BANDS.get(station['band'])} {FREQUENCY_GIGAHERTZ}" + if WIFI_BANDS.get(station["band"]) + else STATE_UNKNOWN + ) + return attrs + + @property + def icon(self) -> str: + """Return device icon.""" + if self.is_connected: + return "mdi:lan-connect" + return "mdi:lan-disconnect" + + @property + def is_connected(self) -> bool: + """Return true if the device is connected to the network.""" + return any( + station + for station in self.coordinator.data[CONNECTED_STATIONS] + if station[MAC_ADDRESS] == self.mac_address + ) + + @property + def mac_address(self) -> str: + """Return mac_address.""" + return self._mac + + @property + def source_type(self) -> str: + """Return tracker source type.""" + return SOURCE_TYPE_ROUTER + + @property + def unique_id(self) -> str: + """Return unique ID of the entity.""" + return f"{self._device.serial_number}_{self._mac}" diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index dd26324bc2c..b7b2275109f 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -15,6 +15,8 @@ from .const import DOMAIN class DevoloEntity(CoordinatorEntity): """Representation of a devolo home network device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, device: Device, device_name: str ) -> None: diff --git a/homeassistant/components/devolo_home_network/translations/ja.json b/homeassistant/components/devolo_home_network/translations/ja.json index ee08879f713..03612804d2d 100644 --- a/homeassistant/components/devolo_home_network/translations/ja.json +++ b/homeassistant/components/devolo_home_network/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002" + "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 7b5ae85bdff..137a884d201 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: COORDINATOR ].async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dexcom/translations/bg.json b/homeassistant/components/dexcom/translations/bg.json index ec574a06c2f..f0b893b6182 100644 --- a/homeassistant/components/dexcom/translations/bg.json +++ b/homeassistant/components/dexcom/translations/bg.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/dexcom/translations/ja.json b/homeassistant/components/dexcom/translations/ja.json index 21cc971beec..5353feeece4 100644 --- a/homeassistant/components/dexcom/translations/ja.json +++ b/homeassistant/components/dexcom/translations/ja.json @@ -16,7 +16,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "Dexcom Share\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3059\u308b", - "title": "Dexcom\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Dexcom\u7d71\u5408\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json index 199db0c326c..8ba9292c8cd 100644 --- a/homeassistant/components/dialogflow/translations/ja.json +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -2,11 +2,11 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { - "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Dialogflow\u306ewebhook\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3]({dialogflow_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Dialogflow\u306ewebhook\u7d71\u5408]({dialogflow_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 1068ec4ccc4..3dfb5708b98 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = dtv - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/discord/translations/pt.json b/homeassistant/components/discord/translations/pt.json new file mode 100644 index 00000000000..9925c2a7416 --- /dev/null +++ b/homeassistant/components/discord/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token" + } + }, + "user": { + "data": { + "api_token": "API Token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 3c3538c1ca0..cc104cc2110 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -61,7 +61,6 @@ SERVICE_HANDLERS = { "yamaha": ServiceDetails("media_player", "yamaha"), "frontier_silicon": ServiceDetails("media_player", "frontier_silicon"), "openhome": ServiceDetails("media_player", "openhome"), - "bose_soundtouch": ServiceDetails("media_player", "soundtouch"), "bluesound": ServiceDetails("media_player", "bluesound"), } @@ -70,6 +69,7 @@ OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {} MIGRATED_SERVICE_HANDLERS = [ SERVICE_APPLE_TV, "axis", + "bose_soundtouch", "deconz", SERVICE_DAIKIN, "denonavr", diff --git a/homeassistant/components/dlna_dmr/__init__.py b/homeassistant/components/dlna_dmr/__init__.py index d34d8550355..e9dd60c5896 100644 --- a/homeassistant/components/dlna_dmr/__init__.py +++ b/homeassistant/components/dlna_dmr/__init__.py @@ -17,7 +17,7 @@ async def async_setup_entry( LOGGER.debug("Setting up config entry: %s", entry.unique_id) # Forward setup to the appropriate platform - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 73e242dbb36..ea153d0d452 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u7d71\u5408\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discovery_error": "\u4e00\u81f4\u3059\u308bDLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "incomplete_config": "\u8a2d\u5b9a\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", diff --git a/homeassistant/components/dlna_dmr/translations/pt.json b/homeassistant/components/dlna_dmr/translations/pt.json new file mode 100644 index 00000000000..49f47abb540 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "confirm": { + "description": "Autentica\u00e7\u00e3o inv\u00e1lid" + }, + "manual": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/pt.json b/homeassistant/components/dlna_dms/translations/pt.json new file mode 100644 index 00000000000..1a5e788da4b --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/__init__.py b/homeassistant/components/dnsip/__init__.py index f679fb4ad30..13783a1b07f 100644 --- a/homeassistant/components/dnsip/__init__.py +++ b/homeassistant/components/dnsip/__init__.py @@ -10,7 +10,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DNS IP from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) return True diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index a770afe388d..93bf73f1b9d 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -52,6 +52,7 @@ class WanIpSensor(SensorEntity): """Implementation of a DNS IP sensor.""" _attr_icon = "mdi:web" + _attr_has_entity_name = True def __init__( self, @@ -61,7 +62,7 @@ class WanIpSensor(SensorEntity): ipv6: bool, ) -> None: """Initialize the DNS IP sensor.""" - self._attr_name = f"{name} IPv6" if ipv6 else name + self._attr_name = "IPv6" if ipv6 else None self._attr_unique_id = f"{hostname}_{ipv6}" self.hostname = hostname self.resolver = aiodns.DNSResolver() @@ -76,7 +77,7 @@ class WanIpSensor(SensorEntity): identifiers={(DOMAIN, f"{hostname}_{ipv6}")}, manufacturer="DNS", model=aiodns.__version__, - name=hostname, + name=name, ) async def async_update(self) -> None: diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index ec170733e44..766167b7af9 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==9.1.1"], + "requirements": ["pydoods==1.0.2", "pillow==9.2.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pydoods"] diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 133c7612a89..62554f9662c 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -146,7 +146,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 0e238363fc0..f3546fc0a00 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -1,19 +1,33 @@ """The dsmr component.""" +from __future__ import annotations + from asyncio import CancelledError from contextlib import suppress +from typing import Any from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er -from .const import DATA_TASK, DOMAIN, PLATFORMS +from .const import CONF_DSMR_VERSION, DATA_TASK, DOMAIN, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DSMR from a config entry.""" + + @callback + def _async_migrate_entity_entry( + entity_entry: er.RegistryEntry, + ) -> dict[str, Any] | None: + """Migrate DSMR entity entry.""" + return async_migrate_entity_entry(entry, entity_entry) + + await er.async_migrate_entries(hass, entry.entry_id, _async_migrate_entity_entry) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True @@ -38,3 +52,76 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) + + +@callback +def async_migrate_entity_entry( + config_entry: ConfigEntry, entity_entry: er.RegistryEntry +) -> dict[str, Any] | None: + """Migrate DSMR entity entries. + + - Migrates unique ID for sensors based on entity description name to key. + """ + + # Replace names with keys in unique ID + for old, new in ( + ("Power_Consumption", "current_electricity_usage"), + ("Power_Production", "current_electricity_delivery"), + ("Power_Tariff", "electricity_active_tariff"), + ("Energy_Consumption_(tarif_1)", "electricity_used_tariff_1"), + ("Energy_Consumption_(tarif_2)", "electricity_used_tariff_2"), + ("Energy_Production_(tarif_1)", "electricity_delivered_tariff_1"), + ("Energy_Production_(tarif_2)", "electricity_delivered_tariff_2"), + ("Power_Consumption_Phase_L1", "instantaneous_active_power_l1_positive"), + ("Power_Consumption_Phase_L3", "instantaneous_active_power_l3_positive"), + ("Power_Consumption_Phase_L2", "instantaneous_active_power_l2_positive"), + ("Power_Production_Phase_L1", "instantaneous_active_power_l1_negative"), + ("Power_Production_Phase_L2", "instantaneous_active_power_l2_negative"), + ("Power_Production_Phase_L3", "instantaneous_active_power_l3_negative"), + ("Short_Power_Failure_Count", "short_power_failure_count"), + ("Long_Power_Failure_Count", "long_power_failure_count"), + ("Voltage_Sags_Phase_L1", "voltage_sag_l1_count"), + ("Voltage_Sags_Phase_L2", "voltage_sag_l2_count"), + ("Voltage_Sags_Phase_L3", "voltage_sag_l3_count"), + ("Voltage_Swells_Phase_L1", "voltage_swell_l1_count"), + ("Voltage_Swells_Phase_L2", "voltage_swell_l2_count"), + ("Voltage_Swells_Phase_L3", "voltage_swell_l3_count"), + ("Voltage_Phase_L1", "instantaneous_voltage_l1"), + ("Voltage_Phase_L2", "instantaneous_voltage_l2"), + ("Voltage_Phase_L3", "instantaneous_voltage_l3"), + ("Current_Phase_L1", "instantaneous_current_l1"), + ("Current_Phase_L2", "instantaneous_current_l2"), + ("Current_Phase_L3", "instantaneous_current_l3"), + ("Max_power_per_phase", "belgium_max_power_per_phase"), + ("Max_current_per_phase", "belgium_max_current_per_phase"), + ("Energy_Consumption_(total)", "electricity_imported_total"), + ("Energy_Production_(total)", "electricity_exported_total"), + ): + if entity_entry.unique_id.endswith(old): + return {"new_unique_id": entity_entry.unique_id.replace(old, new)} + + # Replace unique ID for gas sensors, based on DSMR version + old = "Gas_Consumption" + if entity_entry.unique_id.endswith(old): + dsmr_version = config_entry.data[CONF_DSMR_VERSION] + if dsmr_version in {"4", "5", "5L"}: + return { + "new_unique_id": entity_entry.unique_id.replace( + old, "hourly_gas_meter_reading" + ) + } + if dsmr_version == "5B": + return { + "new_unique_id": entity_entry.unique_id.replace( + old, "belgium_5min_gas_meter_reading" + ) + } + if dsmr_version == "2.2": + return { + "new_unique_id": entity_entry.unique_id.replace( + old, "gas_meter_reading" + ) + } + + # No migration needed + return None diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 9f08e812e04..5e1a54aedc4 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -3,13 +3,7 @@ from __future__ import annotations import logging -from dsmr_parser import obis_references - -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import Platform -from homeassistant.helpers.entity import EntityCategory - -from .models import DSMRSensorEntityDescription DOMAIN = "dsmr" @@ -40,270 +34,3 @@ DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} DSMR_PROTOCOL = "dsmr_protocol" RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol" - -SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( - DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_USAGE, - name="Power Consumption", - device_class=SensorDeviceClass.POWER, - force_update=True, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_DELIVERY, - name="Power Production", - device_class=SensorDeviceClass.POWER, - force_update=True, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_ACTIVE_TARIFF, - name="Power Tariff", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - icon="mdi:flash", - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_1, - name="Energy Consumption (tarif 1)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - device_class=SensorDeviceClass.ENERGY, - force_update=True, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_2, - name="Energy Consumption (tarif 2)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, - name="Energy Production (tarif 1)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, - name="Energy Production (tarif 2)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, - name="Power Consumption Phase L1", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, - name="Power Consumption Phase L2", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, - name="Power Consumption Phase L3", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, - name="Power Production Phase L1", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, - name="Power Production Phase L2", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, - name="Power Production Phase L3", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.SHORT_POWER_FAILURE_COUNT, - name="Short Power Failure Count", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:flash-off", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.LONG_POWER_FAILURE_COUNT, - name="Long Power Failure Count", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:flash-off", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L1_COUNT, - name="Voltage Sags Phase L1", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L2_COUNT, - name="Voltage Sags Phase L2", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L3_COUNT, - name="Voltage Sags Phase L3", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L1_COUNT, - name="Voltage Swells Phase L1", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L2_COUNT, - name="Voltage Swells Phase L2", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L3_COUNT, - name="Voltage Swells Phase L3", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L1, - name="Voltage Phase L1", - device_class=SensorDeviceClass.VOLTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L2, - name="Voltage Phase L2", - device_class=SensorDeviceClass.VOLTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L3, - name="Voltage Phase L3", - device_class=SensorDeviceClass.VOLTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L1, - name="Current Phase L1", - device_class=SensorDeviceClass.CURRENT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L2, - name="Current Phase L2", - device_class=SensorDeviceClass.CURRENT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L3, - name="Current Phase L3", - device_class=SensorDeviceClass.CURRENT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_POWER_PER_PHASE, - name="Max power per phase", - dsmr_versions={"5B"}, - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, - name="Max current per phase", - dsmr_versions={"5B"}, - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_IMPORTED_TOTAL, - name="Energy Consumption (total)", - dsmr_versions={"5L", "5S", "Q3D"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_EXPORTED_TOTAL, - name="Energy Production (total)", - dsmr_versions={"5L", "5S", "Q3D"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.HOURLY_GAS_METER_READING, - name="Gas Consumption", - dsmr_versions={"4", "5", "5L"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.BELGIUM_5MIN_GAS_METER_READING, - name="Gas Consumption", - dsmr_versions={"5B"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.GAS_METER_READING, - name="Gas Consumption", - dsmr_versions={"2.2"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), -) diff --git a/homeassistant/components/dsmr/models.py b/homeassistant/components/dsmr/models.py deleted file mode 100644 index e7b47d8b74d..00000000000 --- a/homeassistant/components/dsmr/models.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Models for the DSMR integration.""" -from __future__ import annotations - -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class DSMRSensorEntityDescription(SensorEntityDescription): - """Represents an DSMR Sensor.""" - - dsmr_versions: set[str] | None = None - is_gas: bool = False diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 9c684493a3f..aa01c798072 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -4,10 +4,11 @@ from __future__ import annotations import asyncio from asyncio import CancelledError from contextlib import suppress +from dataclasses import dataclass from datetime import timedelta from functools import partial -from dsmr_parser import obis_references as obis_ref +from dsmr_parser import obis_references from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader from dsmr_parser.clients.rfxtrx_protocol import ( create_rfxtrx_dsmr_reader, @@ -16,7 +17,12 @@ from dsmr_parser.clients.rfxtrx_protocol import ( from dsmr_parser.objects import DSMRObject import serial -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -25,7 +31,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import EventType, StateType from homeassistant.util import Throttle @@ -47,13 +53,330 @@ from .const import ( DOMAIN, DSMR_PROTOCOL, LOGGER, - SENSORS, ) -from .models import DSMRSensorEntityDescription UNIT_CONVERSION = {"m3": VOLUME_CUBIC_METERS} +@dataclass +class DSMRSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + obis_reference: str + + +@dataclass +class DSMRSensorEntityDescription( + SensorEntityDescription, DSMRSensorEntityDescriptionMixin +): + """Represents an DSMR Sensor.""" + + dsmr_versions: set[str] | None = None + is_gas: bool = False + + +SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( + DSMRSensorEntityDescription( + key="current_electricity_usage", + name="Power consumption", + obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE, + device_class=SensorDeviceClass.POWER, + force_update=True, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key="current_electricity_delivery", + name="Power production", + obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, + device_class=SensorDeviceClass.POWER, + force_update=True, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key="electricity_active_tariff", + name="Active tariff", + obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + icon="mdi:flash", + ), + DSMRSensorEntityDescription( + key="electricity_used_tariff_1", + name="Energy consumption (tarif 1)", + obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + device_class=SensorDeviceClass.ENERGY, + force_update=True, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key="electricity_used_tariff_2", + name="Energy consumption (tarif 2)", + obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key="electricity_delivered_tariff_1", + name="Energy production (tarif 1)", + obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key="electricity_delivered_tariff_2", + name="Energy production (tarif 2)", + obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key="instantaneous_active_power_l1_positive", + name="Power consumption phase L1", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key="instantaneous_active_power_l2_positive", + name="Power consumption phase L2", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key="instantaneous_active_power_l3_positive", + name="Power consumption phase L3", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key="instantaneous_active_power_l1_negative", + name="Power production phase L1", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key="instantaneous_active_power_l2_negative", + name="Power production phase L2", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key="instantaneous_active_power_l3_negative", + name="Power production phase L3", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key="short_power_failure_count", + name="Short power failure count", + obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:flash-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="long_power_failure_count", + name="Long power failure count", + obis_reference=obis_references.LONG_POWER_FAILURE_COUNT, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:flash-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="voltage_sag_l1_count", + name="Voltage sags phase L1", + obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="voltage_sag_l2_count", + name="Voltage sags phase L2", + obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="voltage_sag_l3_count", + name="Voltage sags phase L3", + obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="voltage_swell_l1_count", + name="Voltage swells phase L1", + obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="voltage_swell_l2_count", + name="Voltage swells phase L2", + obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="voltage_swell_l3_count", + name="Voltage swells phase L3", + obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT, + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="instantaneous_voltage_l1", + name="Voltage phase L1", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1, + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="instantaneous_voltage_l2", + name="Voltage phase L2", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2, + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="instantaneous_voltage_l3", + name="Voltage phase L3", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3, + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="instantaneous_current_l1", + name="Current phase L1", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1, + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="instantaneous_current_l2", + name="Current phase L2", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2, + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="instantaneous_current_l3", + name="Current phase L3", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3, + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="belgium_max_power_per_phase", + name="Max power per phase", + obis_reference=obis_references.BELGIUM_MAX_POWER_PER_PHASE, + dsmr_versions={"5B"}, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="belgium_max_current_per_phase", + name="Max current per phase", + obis_reference=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, + dsmr_versions={"5B"}, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key="electricity_imported_total", + name="Energy consumption (total)", + obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL, + dsmr_versions={"5L", "5S", "Q3D"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key="electricity_exported_total", + name="Energy production (total)", + obis_reference=obis_references.ELECTRICITY_EXPORTED_TOTAL, + dsmr_versions={"5L", "5S", "Q3D"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key="hourly_gas_meter_reading", + name="Gas consumption", + obis_reference=obis_references.HOURLY_GAS_METER_READING, + dsmr_versions={"4", "5", "5L"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key="belgium_5min_gas_meter_reading", + name="Gas consumption", + obis_reference=obis_references.BELGIUM_5MIN_GAS_METER_READING, + dsmr_versions={"5B"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key="gas_meter_reading", + name="Gas consumption", + obis_reference=obis_references.GAS_METER_READING, + dsmr_versions={"2.2"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -190,6 +513,7 @@ class DSMREntity(SensorEntity): """Entity reading values from DSMR telegram.""" entity_description: DSMRSensorEntityDescription + _attr_has_entity_name = True _attr_should_poll = False def __init__( @@ -212,25 +536,23 @@ class DSMREntity(SensorEntity): identifiers={(DOMAIN, device_serial)}, name=device_name, ) - self._attr_unique_id = f"{device_serial}_{entity_description.name}".replace( - " ", "_" - ) + self._attr_unique_id = f"{device_serial}_{entity_description.key}" @callback def update_data(self, telegram: dict[str, DSMRObject]) -> None: """Update data.""" self.telegram = telegram - if self.hass and self.entity_description.key in self.telegram: + if self.hass and self.entity_description.obis_reference in self.telegram: self.async_write_ha_state() def get_dsmr_object_attr(self, attribute: str) -> str | None: """Read attribute from last received telegram for this DSMR object.""" # Make sure telegram contains an object for this entities obis - if self.entity_description.key not in self.telegram: + if self.entity_description.obis_reference not in self.telegram: return None # Get the attribute value if the object has it - dsmr_object = self.telegram[self.entity_description.key] + dsmr_object = self.telegram[self.entity_description.obis_reference] attr: str | None = getattr(dsmr_object, attribute) return attr @@ -240,7 +562,10 @@ class DSMREntity(SensorEntity): if (value := self.get_dsmr_object_attr("value")) is None: return None - if self.entity_description.key == obis_ref.ELECTRICITY_ACTIVE_TARIFF: + if ( + self.entity_description.obis_reference + == obis_references.ELECTRICITY_ACTIVE_TARIFF + ): return self.translate_tariff(value, self._entry.data[CONF_DSMR_VERSION]) with suppress(TypeError): diff --git a/homeassistant/components/dsmr/translations/pt.json b/homeassistant/components/dsmr/translations/pt.json index ce8a9287272..311a677d6de 100644 --- a/homeassistant/components/dsmr/translations/pt.json +++ b/homeassistant/components/dsmr/translations/pt.json @@ -1,7 +1,24 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "setup_network": { + "data": { + "host": "Servidor" + }, + "title": "Seleccione o endere\u00e7o de liga\u00e7\u00e3o" + }, + "setup_serial_manual_path": { + "data": { + "port": "Caminho do Dispositivo USB" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 9f4ee7ed918..ac61837afec 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -225,42 +225,36 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Low tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2", name="High tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_returned", name="Low tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2_returned", name="High tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_merged", name="Power usage total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_returned_merged", name="Power return total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_cost", diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index 839f79bc3f4..c97e27f4017 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = player - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dunehd/translations/ja.json b/homeassistant/components/dunehd/translations/ja.json index c82d7ce8f94..ebfa6f0c0ac 100644 --- a/homeassistant/components/dunehd/translations/ja.json +++ b/homeassistant/components/dunehd/translations/ja.json @@ -13,7 +13,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "Dune HD\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 35f76354eab..fe1872e1fe3 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -275,7 +275,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = None raise ConfigEntryNotReady - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/eafm/__init__.py b/homeassistant/components/eafm/__init__.py index e7b6cd88092..a57558ff1cc 100644 --- a/homeassistant/components/eafm/__init__.py +++ b/homeassistant/components/eafm/__init__.py @@ -11,7 +11,7 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up flood monitoring sensors for this config entry.""" hass.data.setdefault(DOMAIN, {}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index ceab670fa6e..7204dbf8de2 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = data - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ecobee/translations/hu.json b/homeassistant/components/ecobee/translations/hu.json index a2fdd5553a4..590cc066de9 100644 --- a/homeassistant/components/ecobee/translations/hu.json +++ b/homeassistant/components/ecobee/translations/hu.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "K\u00e9rj\u00fck, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n {pin} \n \nEzut\u00e1n nyomja meg a Mehet gombot.", + "description": "K\u00e9rem, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n{pin} \n \nEzut\u00e1n nyomja meg a Mehet gombot.", "title": "Alkalmaz\u00e1s enged\u00e9lyez\u00e9se ecobee.com-on" }, "user": { diff --git a/homeassistant/components/ecobee/translations/ja.json b/homeassistant/components/ecobee/translations/ja.json index 73ac4cd1611..79f8add6a4d 100644 --- a/homeassistant/components/ecobee/translations/ja.json +++ b/homeassistant/components/ecobee/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "pin_request_failed": "ecobee\u304b\u3089\u306ePIN\u30ea\u30af\u30a8\u30b9\u30c8\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f; API\u30ad\u30fc\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index a706ceb8e7e..728222dcda7 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api hass.data[DOMAIN][EQUIPMENT][config_entry.entry_id] = equipment - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) api.subscribe() diff --git a/homeassistant/components/econet/translations/pt.json b/homeassistant/components/econet/translations/pt.json new file mode 100644 index 00000000000..25aaf514180 --- /dev/null +++ b/homeassistant/components/econet/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 915eb0daf46..ce8483672a2 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/efergy/translations/pt.json b/homeassistant/components/efergy/translations/pt.json new file mode 100644 index 00000000000..db89dfe29e6 --- /dev/null +++ b/homeassistant/components/efergy/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 5cd7bec9244..67ff6c59a54 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: eight, heat_coordinator, user_coordinator ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/eight_sleep/translations/pt.json b/homeassistant/components/eight_sleep/translations/pt.json new file mode 100644 index 00000000000..bcb163d8c10 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/ru.json b/homeassistant/components/eight_sleep/translations/ru.json new file mode 100644 index 00000000000..0370f202938 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043e\u0431\u043b\u0430\u043a\u0443 Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043e\u0431\u043b\u0430\u043a\u0443 Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index f15ccc0a03d..c2d70c69735 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: info=info, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/elgato/entity.py b/homeassistant/components/elgato/entity.py index 8927f8d71c0..16d3ac78e81 100644 --- a/homeassistant/components/elgato/entity.py +++ b/homeassistant/components/elgato/entity.py @@ -12,6 +12,8 @@ from .const import DOMAIN class ElgatoEntity(Entity): """Defines an Elgato entity.""" + _attr_has_entity_name = True + def __init__(self, client: Elgato, info: Info, mac: str | None) -> None: """Initialize an Elgato entity.""" self.client = client diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index e13119c2887..2a9f63a83d7 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -80,7 +80,6 @@ class ElgatoLight( self._attr_min_mireds = 143 self._attr_max_mireds = 344 - self._attr_name = info.display_name or info.product_name self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} self._attr_unique_id = info.serial_number diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index cf1cac3bdb3..2ce0e726fc4 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -74,6 +74,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT, Platform.SCENE, @@ -303,7 +304,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "keypads": {}, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -442,13 +443,14 @@ def create_elk_entities( class ElkEntity(Entity): """Base class for all Elk entities.""" + _attr_has_entity_name = True + def __init__(self, element: Element, elk: Elk, elk_data: dict[str, Any]) -> None: """Initialize the base of all Elk devices.""" self._elk = elk self._element = element self._mac = elk_data["mac"] self._prefix = elk_data["prefix"] - self._name_prefix = f"{self._prefix} " if self._prefix else "" self._temperature_unit: str = elk_data["config"]["temperature_unit"] # unique_id starts with elkm1_ iff there is no prefix # it starts with elkm1m_{prefix} iff there is a prefix @@ -463,11 +465,7 @@ class ElkEntity(Entity): else: uid_start = "elkm1" self._unique_id = f"{uid_start}_{self._element.default_name('_')}".lower() - - @property - def name(self) -> str: - """Name of the element.""" - return f"{self._name_prefix}{self._element.name}" + self._attr_name = element.name @property def unique_id(self) -> str: diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 6b6a5b44d55..3f5163a849d 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -69,7 +69,7 @@ async def async_setup_entry( elk = elk_data["elk"] entities: list[ElkEntity] = [] create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities) - async_add_entities(entities, True) + async_add_entities(entities) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/elkm1/binary_sensor.py b/homeassistant/components/elkm1/binary_sensor.py new file mode 100644 index 00000000000..38a72796482 --- /dev/null +++ b/homeassistant/components/elkm1/binary_sensor.py @@ -0,0 +1,57 @@ +"""Support for control of ElkM1 binary sensors.""" +from __future__ import annotations + +from typing import Any + +from elkm1_lib.const import ZoneLogicalStatus, ZoneType +from elkm1_lib.elements import Element +from elkm1_lib.zones import Zone + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ElkAttachedEntity, ElkEntity +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Create the Elk-M1 sensor platform.""" + + elk_data = hass.data[DOMAIN][config_entry.entry_id] + auto_configure = elk_data["auto_configure"] + elk = elk_data["elk"] + + entities: list[ElkEntity] = [] + for element in elk.zones: + # Don't create binary sensors for zones that are analog + if element.definition in {ZoneType.TEMPERATURE, ZoneType.ANALOG_ZONE}: + continue + + if auto_configure: + if not element.configured: + continue + elif not elk_data["config"]["zone"]["included"][element.index]: + continue + + entities.append(ElkBinarySensor(element, elk, elk_data)) + + async_add_entities(entities) + + +class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity): + """Representation of ElkM1 binary sensor.""" + + _element: Zone + _attr_entity_registry_enabled_default = False + + def _element_changed(self, _: Element, changeset: Any) -> None: + # Zone in NORMAL state is OFF; any other state is ON + self._attr_is_on = bool( + self._element.logical_status != ZoneLogicalStatus.NORMAL + ) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 9f6dc359f6f..8bbf776c475 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -68,7 +68,7 @@ async def async_setup_entry( create_elk_entities( elk_data, elk.thermostats, "thermostat", ElkThermostat, entities ) - async_add_entities(entities, True) + async_add_entities(entities) class ElkThermostat(ElkEntity, ClimateEntity): diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py index 9e008359e8c..3db457761aa 100644 --- a/homeassistant/components/elkm1/light.py +++ b/homeassistant/components/elkm1/light.py @@ -26,7 +26,7 @@ async def async_setup_entry( entities: list[ElkEntity] = [] elk = elk_data["elk"] create_elk_entities(elk_data, elk.lights, "plc", ElkLight, entities) - async_add_entities(entities, True) + async_add_entities(entities) class ElkLight(ElkEntity, LightEntity): diff --git a/homeassistant/components/elkm1/scene.py b/homeassistant/components/elkm1/scene.py index d8100c5bcb1..1869e5ba0f3 100644 --- a/homeassistant/components/elkm1/scene.py +++ b/homeassistant/components/elkm1/scene.py @@ -24,7 +24,7 @@ async def async_setup_entry( entities: list[ElkEntity] = [] elk = elk_data["elk"] create_elk_entities(elk_data, elk.tasks, "task", ElkTask, entities) - async_add_entities(entities, True) + async_add_entities(entities) class ElkTask(ElkAttachedEntity, Scene): diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 57f989d5cb5..1d84af259ee 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -51,7 +51,7 @@ async def async_setup_entry( create_elk_entities(elk_data, [elk.panel], "panel", ElkPanel, entities) create_elk_entities(elk_data, elk.settings, "setting", ElkSetting, entities) create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities) - async_add_entities(entities, True) + async_add_entities(entities) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py index 54588958e61..a17557b1507 100644 --- a/homeassistant/components/elkm1/switch.py +++ b/homeassistant/components/elkm1/switch.py @@ -24,7 +24,7 @@ async def async_setup_entry( entities: list[ElkEntity] = [] elk = elk_data["elk"] create_elk_entities(elk_data, elk.outputs, "output", ElkOutput, entities) - async_add_entities(entities, True) + async_add_entities(entities) class ElkOutput(ElkAttachedEntity, SwitchEntity): diff --git a/homeassistant/components/elkm1/translations/pt.json b/homeassistant/components/elkm1/translations/pt.json index 2e669c21f1e..08fe97d2354 100644 --- a/homeassistant/components/elkm1/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt.json @@ -4,6 +4,21 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "discovered_connection": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + }, + "title": "Ligar ao Controlo Elk-M1" + }, + "manual_connection": { + "data": { + "username": "Nome de Utilizador" + }, + "title": "Ligar ao Controlo Elk-M1" + } } } } \ No newline at end of file diff --git a/homeassistant/components/elmax/__init__.py b/homeassistant/components/elmax/__init__.py index af123efae9a..0c0a80b4958 100644 --- a/homeassistant/components/elmax/__init__.py +++ b/homeassistant/components/elmax/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator # Perform platform initialization. - hass.config_entries.async_setup_platforms(entry, ELMAX_PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, ELMAX_PLATFORMS) return True diff --git a/homeassistant/components/elmax/binary_sensor.py b/homeassistant/components/elmax/binary_sensor.py new file mode 100644 index 00000000000..71588b4687f --- /dev/null +++ b/homeassistant/components/elmax/binary_sensor.py @@ -0,0 +1,68 @@ +"""Elmax sensor platform.""" +from __future__ import annotations + +from elmax_api.model.panel import PanelStatus + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ElmaxCoordinator +from .common import ElmaxEntity +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Elmax sensor platform.""" + coordinator: ElmaxCoordinator = hass.data[DOMAIN][config_entry.entry_id] + known_devices = set() + + def _discover_new_devices(): + panel_status: PanelStatus = coordinator.data + # In case the panel is offline, its status will be None. In that case, simply do nothing + if panel_status is None: + return + + # Otherwise, add all the entities we found + entities = [] + for zone in panel_status.zones: + # Skip already handled devices + if zone.endpoint_id in known_devices: + continue + entity = ElmaxSensor( + panel=coordinator.panel_entry, + elmax_device=zone, + panel_version=panel_status.release, + coordinator=coordinator, + ) + entities.append(entity) + async_add_entities(entities, True) + known_devices.update([e.unique_id for e in entities]) + + # Register a listener for the discovery of new devices + coordinator.async_add_listener(_discover_new_devices) + + # Immediately run a discovery, so we don't need to wait for the next update + _discover_new_devices() + + +class ElmaxSensor(ElmaxEntity, BinarySensorEntity): + """Elmax Sensor entity implementation.""" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.coordinator.get_zone_state(self._device.endpoint_id).opened + + @property + def device_class(self) -> BinarySensorDeviceClass: + """Return the class of this device, from component DEVICE_CLASSES.""" + return BinarySensorDeviceClass.DOOR diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index 2d66ca9f72e..4116ff05f44 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -65,6 +65,12 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): return self._state_by_endpoint.get(actuator_id) raise HomeAssistantError("Unknown actuator") + def get_zone_state(self, zone_id: str) -> Actuator: + """Return state of a specific zone.""" + if self._state_by_endpoint is not None: + return self._state_by_endpoint.get(zone_id) + raise HomeAssistantError("Unknown zone") + @property def http_client(self): """Return the current http client being used by this instance.""" diff --git a/homeassistant/components/elmax/const.py b/homeassistant/components/elmax/const.py index 21864e98f1a..514412d6897 100644 --- a/homeassistant/components/elmax/const.py +++ b/homeassistant/components/elmax/const.py @@ -11,7 +11,7 @@ CONF_ELMAX_PANEL_NAME = "panel_name" CONF_CONFIG_ENTRY_ID = "config_entry_id" CONF_ENDPOINT_ID = "endpoint_id" -ELMAX_PLATFORMS = [Platform.SWITCH] +ELMAX_PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR] POLLING_SECONDS = 30 DEFAULT_TIMEOUT = 10.0 diff --git a/homeassistant/components/elmax/translations/ja.json b/homeassistant/components/elmax/translations/ja.json index 8730ae4791b..2ae13e7a895 100644 --- a/homeassistant/components/elmax/translations/ja.json +++ b/homeassistant/components/elmax/translations/ja.json @@ -17,7 +17,7 @@ "panel_name": "\u30d1\u30cd\u30eb\u540d", "panel_pin": "PIN\u30b3\u30fc\u30c9" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u5236\u5fa1\u3059\u308b\u30d1\u30cd\u30eb\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001\u30d1\u30cd\u30eb\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308b\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u3053\u306e\u7d71\u5408\u3067\u5236\u5fa1\u3059\u308b\u30d1\u30cd\u30eb\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001\u30d1\u30cd\u30eb\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308b\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { "data": { diff --git a/homeassistant/components/elmax/translations/pt.json b/homeassistant/components/elmax/translations/pt.json new file mode 100644 index 00000000000..bcb163d8c10 --- /dev/null +++ b/homeassistant/components/elmax/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 3d03c7b8fe6..ea19808cd37 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/emonitor/translations/pt.json b/homeassistant/components/emonitor/translations/pt.json new file mode 100644 index 00000000000..04374af8e82 --- /dev/null +++ b/homeassistant/components/emonitor/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index d33f915628d..3b3952136be 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import Counter from collections.abc import Awaitable, Callable -from typing import Literal, Optional, TypedDict, Union, cast +from typing import Literal, TypedDict, Union import voluptuous as vol @@ -54,7 +54,7 @@ class FlowToGridSourceType(TypedDict): stat_compensation: str | None # Used to generate costs if stat_compensation is set to None - entity_energy_from: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_from + entity_energy_to: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_to entity_energy_price: str | None # entity_id of an entity providing price ($/kWh) number_energy_price: float | None # Price for energy ($/kWh) @@ -263,13 +263,15 @@ class EnergyManager: def __init__(self, hass: HomeAssistant) -> None: """Initialize energy manager.""" self._hass = hass - self._store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = storage.Store[EnergyPreferences]( + hass, STORAGE_VERSION, STORAGE_KEY + ) self.data: EnergyPreferences | None = None self._update_listeners: list[Callable[[], Awaitable]] = [] async def async_initialize(self) -> None: """Initialize the energy integration.""" - self.data = cast(Optional[EnergyPreferences], await self._store.async_load()) + self.data = await self._store.async_load() @staticmethod def default_preferences() -> EnergyPreferences: @@ -294,7 +296,7 @@ class EnergyManager: data[key] = update[key] # type: ignore[literal-required] self.data = data - self._store.async_delay_save(lambda: cast(dict, self.data), 60) + self._store.async_delay_save(lambda: data, 60) if not self._update_listeners: return diff --git a/homeassistant/components/energy/translations/pt.json b/homeassistant/components/energy/translations/pt.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/pt.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 3baae348770..9d6b3bd53c7 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -40,7 +40,11 @@ GAS_USAGE_DEVICE_CLASSES = ( sensor.SensorDeviceClass.GAS, ) GAS_USAGE_UNITS = { - sensor.SensorDeviceClass.ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), + sensor.SensorDeviceClass.ENERGY: ( + ENERGY_WATT_HOUR, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ), sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET), } GAS_PRICE_UNITS = tuple( @@ -263,10 +267,10 @@ def _async_validate_auto_generated_cost_entity( async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: """Validate the energy configuration.""" - manager = await data.async_get_manager(hass) + manager: data.EnergyManager = await data.async_get_manager(hass) statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]] = {} validate_calls = [] - wanted_statistics_metadata = set() + wanted_statistics_metadata: set[str] = set() result = EnergyPreferencesValidation() @@ -279,6 +283,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: result.energy_sources.append(source_result) if source["type"] == "grid": + flow: data.FlowFromGridSourceType | data.FlowToGridSourceType for flow in source["flow_from"]: wanted_statistics_metadata.add(flow["stat_energy_from"]) validate_calls.append( @@ -294,14 +299,14 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("stat_cost") is not None: - wanted_statistics_metadata.add(flow["stat_cost"]) + if (stat_cost := flow.get("stat_cost")) is not None: + wanted_statistics_metadata.add(stat_cost) validate_calls.append( functools.partial( _async_validate_cost_stat, hass, statistics_metadata, - flow["stat_cost"], + stat_cost, source_result, ) ) @@ -345,14 +350,14 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("stat_compensation") is not None: - wanted_statistics_metadata.add(flow["stat_compensation"]) + if (stat_compensation := flow.get("stat_compensation")) is not None: + wanted_statistics_metadata.add(stat_compensation) validate_calls.append( functools.partial( _async_validate_cost_stat, hass, statistics_metadata, - flow["stat_compensation"], + stat_compensation, source_result, ) ) @@ -396,14 +401,14 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if source.get("stat_cost") is not None: - wanted_statistics_metadata.add(source["stat_cost"]) + if (stat_cost := source.get("stat_cost")) is not None: + wanted_statistics_metadata.add(stat_cost) validate_calls.append( functools.partial( _async_validate_cost_stat, hass, statistics_metadata, - source["stat_cost"], + stat_cost, source_result, ) ) diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index a53f691df19..5edd2bb6155 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -5,12 +5,14 @@ from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_ID, CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import DOMAIN, LOGGER from .device import EnOceanEntity CONF_CHANNEL = "channel" @@ -25,10 +27,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +def generate_unique_id(dev_id: list[int], channel: int) -> str: + """Generate a valid unique id.""" + return f"{combine_hex(dev_id)}-{channel}" + + +def _migrate_to_new_unique_id(hass: HomeAssistant, dev_id, channel) -> None: + """Migrate old unique ids to new unique ids.""" + old_unique_id = f"{combine_hex(dev_id)}" + + ent_reg = entity_registry.async_get(hass) + entity_id = ent_reg.async_get_entity_id(Platform.SWITCH, DOMAIN, old_unique_id) + + if entity_id is not None: + new_unique_id = generate_unique_id(dev_id, channel) + try: + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + except ValueError: + LOGGER.warning( + "Skip migration of id [%s] to [%s] because it already exists", + old_unique_id, + new_unique_id, + ) + else: + LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) + + +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the EnOcean switch platform.""" @@ -36,7 +68,8 @@ def setup_platform( dev_id = config.get(CONF_ID) dev_name = config.get(CONF_NAME) - add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) + _migrate_to_new_unique_id(hass, dev_id, channel) + async_add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) class EnOceanSwitch(EnOceanEntity, SwitchEntity): @@ -49,7 +82,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): self._on_state = False self._on_state2 = False self.channel = channel - self._attr_unique_id = f"{combine_hex(dev_id)}" + self._attr_unique_id = generate_unique_id(dev_id, channel) @property def is_on(self): diff --git a/homeassistant/components/enocean/translations/ja.json b/homeassistant/components/enocean/translations/ja.json index e0ec74d778f..bc71afe3a27 100644 --- a/homeassistant/components/enocean/translations/ja.json +++ b/homeassistant/components/enocean/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_dongle_path": "\u30c9\u30f3\u30b0\u30eb\u30d1\u30b9\u304c\u7121\u52b9", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_dongle_path": "\u3053\u306e\u30d1\u30b9\u306b\u6709\u52b9\u306a\u30c9\u30f3\u30b0\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 0c6c893df64..61c2fd86c77 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: NAME: name, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/enphase_envoy/translations/bg.json b/homeassistant/components/enphase_envoy/translations/bg.json index ffb593eb287..1fce5cf396e 100644 --- a/homeassistant/components/enphase_envoy/translations/bg.json +++ b/homeassistant/components/enphase_envoy/translations/bg.json @@ -5,6 +5,13 @@ }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/pt.json b/homeassistant/components/enphase_envoy/translations/pt.json new file mode 100644 index 00000000000..1aa61659a93 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index c7f4fbeef53..3bbacb8c3d4 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "entur_public_transport", "name": "Entur", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", - "requirements": ["enturclient==0.2.3"], + "requirements": ["enturclient==0.2.4"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_polling", "loggers": ["enturclient"] diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 4ed1f5d0fbb..a8548429d50 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -9,6 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN @@ -71,7 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinators - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True @@ -87,6 +89,17 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +def device_info(config_entry: ConfigEntry) -> DeviceInfo: + """Build and return the device info for EC.""" + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer="Environment Canada", + name=config_entry.title, + configuration_url="https://weather.gc.ca/", + ) + + class ECDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching EC data.""" diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index e415bab977b..7b93f0b28f4 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -12,6 +12,7 @@ from homeassistant.helpers.entity_platform import ( ) from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import device_info from .const import ATTR_OBSERVATION_TIME, DOMAIN SERVICE_SET_RADAR_TYPE = "set_radar_type" @@ -40,16 +41,19 @@ async def async_setup_entry( class ECCamera(CoordinatorEntity, Camera): """Implementation of an Environment Canada radar camera.""" + _attr_has_entity_name = True + _attr_name = "Radar" + def __init__(self, coordinator): """Initialize the camera.""" super().__init__(coordinator) Camera.__init__(self) self.radar_object = coordinator.ec_data - self._attr_name = f"{coordinator.config_entry.title} Radar" self._attr_unique_id = f"{coordinator.config_entry.unique_id}-radar" self._attr_attribution = self.radar_object.metadata["attribution"] self._attr_entity_registry_enabled_default = False + self._attr_device_info = device_info(coordinator.config_entry) self.content_type = "image/gif" diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 2e124d1ec7c..08da60fe01f 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -27,6 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import device_info from .const import ATTR_STATION, DOMAIN ATTR_TIME = "alert time" @@ -51,12 +52,12 @@ class ECSensorEntityDescription( SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ECSensorEntityDescription( key="condition", - name="Current Condition", + name="Current condition", value_fn=lambda data: data.conditions.get("condition", {}).get("value"), ), ECSensorEntityDescription( key="dewpoint", - name="Dew Point", + name="Dew point", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -64,7 +65,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="high_temp", - name="High Temperature", + name="High temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -88,12 +89,12 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="icon_code", - name="Icon Code", + name="Icon code", value_fn=lambda data: data.conditions.get("icon_code", {}).get("value"), ), ECSensorEntityDescription( key="low_temp", - name="Low Temperature", + name="Low temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -101,34 +102,34 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="normal_high", - name="Normal High Temperature", + name="Normal high temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, value_fn=lambda data: data.conditions.get("normal_high", {}).get("value"), ), ECSensorEntityDescription( key="normal_low", - name="Normal Low Temperature", + name="Normal low temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, value_fn=lambda data: data.conditions.get("normal_low", {}).get("value"), ), ECSensorEntityDescription( key="pop", - name="Chance of Precipitation", + name="Chance of precipitation", native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: data.conditions.get("pop", {}).get("value"), ), ECSensorEntityDescription( key="precip_yesterday", - name="Precipitation Yesterday", + name="Precipitation yesterday", native_unit_of_measurement=LENGTH_MILLIMETERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"), ), ECSensorEntityDescription( key="pressure", - name="Barometric Pressure", + name="Barometric pressure", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_KPA, state_class=SensorStateClass.MEASUREMENT, @@ -156,13 +157,13 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="timestamp", - name="Observation Time", + name="Observation time", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.metadata.get("timestamp"), ), ECSensorEntityDescription( key="uv_index", - name="UV Index", + name="UV index", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("uv_index", {}).get("value"), @@ -176,13 +177,13 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="wind_bearing", - name="Wind Bearing", + name="Wind bearing", native_unit_of_measurement=DEGREE, value_fn=lambda data: data.conditions.get("wind_bearing", {}).get("value"), ), ECSensorEntityDescription( key="wind_chill", - name="Wind Chill", + name="Wind chill", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -190,19 +191,19 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="wind_dir", - name="Wind Direction", + name="Wind direction", value_fn=lambda data: data.conditions.get("wind_dir", {}).get("value"), ), ECSensorEntityDescription( key="wind_gust", - name="Wind Gust", + name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_gust", {}).get("value"), ), ECSensorEntityDescription( key="wind_speed", - name="Wind Speed", + name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_speed", {}).get("value"), @@ -285,6 +286,7 @@ class ECBaseSensor(CoordinatorEntity, SensorEntity): """Environment Canada sensor base.""" entity_description: ECSensorEntityDescription + _attr_has_entity_name = True def __init__(self, coordinator, description): """Initialize the base sensor.""" @@ -292,8 +294,8 @@ class ECBaseSensor(CoordinatorEntity, SensorEntity): self.entity_description = description self._ec_data = coordinator.ec_data self._attr_attribution = self._ec_data.metadata["attribution"] - self._attr_name = f"{coordinator.config_entry.title} {description.name}" self._attr_unique_id = f"{coordinator.config_entry.title}-{description.key}" + self._attr_device_info = device_info(coordinator.config_entry) @property def native_value(self): diff --git a/homeassistant/components/environment_canada/translations/pt.json b/homeassistant/components/environment_canada/translations/pt.json new file mode 100644 index 00000000000..c7081cd694a --- /dev/null +++ b/homeassistant/components/environment_canada/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 40706ffb6c1..8dbf8c15731 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -35,6 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt +from . import device_info from .const import DOMAIN # Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/ @@ -68,6 +69,7 @@ async def async_setup_entry( class ECWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_has_entity_name = True _attr_native_pressure_unit = PRESSURE_KPA _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_visibility_unit = LENGTH_KILOMETERS @@ -78,14 +80,13 @@ class ECWeather(CoordinatorEntity, WeatherEntity): super().__init__(coordinator) self.ec_data = coordinator.ec_data self._attr_attribution = self.ec_data.metadata["attribution"] - self._attr_name = ( - f"{coordinator.config_entry.title}{' Hourly' if hourly else ''}" - ) + self._attr_name = "Hourly forecast" if hourly else "Forecast" self._attr_unique_id = ( f"{coordinator.config_entry.unique_id}{'-hourly' if hourly else '-daily'}" ) self._attr_entity_registry_enabled_default = not hourly self._hourly = hourly + self._attr_device_info = device_info(coordinator.config_entry) @property def native_temperature(self): diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index 9710cb8d96a..5a8544c7bf1 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = projector - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 1f95f03bd17..412bb8eddeb 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from bluepy.btle import BTLEException # pylint: disable=import-error import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol @@ -218,5 +217,5 @@ class EQ3BTSmartThermostat(ClimateEntity): try: self._thermostat.update() - except BTLEException as ex: + except eq3.BackendException as ex: _LOGGER.warning("Updating the state failed: %s", ex) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 4ad8d08adf5..ade3bc0d912 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -1,9 +1,10 @@ { "domain": "eq3btsmart", - "name": "EQ3 Bluetooth Smart Thermostats", + "name": "eQ-3 Bluetooth Smart Thermostats", "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", - "requirements": ["construct==2.10.56", "python-eq3bt==0.1.11"], + "requirements": ["construct==2.10.56", "python-eq3bt==0.2"], + "dependencies": ["bluetooth"], "codeowners": ["@rytilahti"], "iot_class": "local_polling", - "loggers": ["bluepy", "eq3bt"] + "loggers": ["bleak", "eq3bt"] } diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index ddedaf11ceb..5d7b0efc18d 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -58,7 +58,7 @@ from .entry_data import RuntimeEntryData DOMAIN = "esphome" CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) -_T = TypeVar("_T") +_DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") STORAGE_VERSION = 1 @@ -101,11 +101,11 @@ class DomainData: ) @classmethod - def get(cls: type[_T], hass: HomeAssistant) -> _T: + def get(cls: type[_DomainDataSelfT], hass: HomeAssistant) -> _DomainDataSelfT: """Get the global DomainData instance stored in hass.data.""" # Don't use setdefault - this is a hot code path if DOMAIN in hass.data: - return cast(_T, hass.data[DOMAIN]) + return cast(_DomainDataSelfT, hass.data[DOMAIN]) ret = hass.data[DOMAIN] = cls() return ret @@ -321,20 +321,17 @@ async def async_setup_entry( # noqa: C901 on_connect_error=on_connect_error, ) - async def complete_setup() -> None: - """Complete the config entry setup.""" - infos, services = await entry_data.async_load_from_store() - await entry_data.async_update_static_infos(hass, entry, infos) - await _setup_services(hass, entry_data, services) + infos, services = await entry_data.async_load_from_store() + await entry_data.async_update_static_infos(hass, entry, infos) + await _setup_services(hass, entry_data, services) - if entry_data.device_info is not None and entry_data.device_info.name: - cli.expected_name = entry_data.device_info.name - reconnect_logic.name = entry_data.device_info.name + if entry_data.device_info is not None and entry_data.device_info.name: + cli.expected_name = entry_data.device_info.name + reconnect_logic.name = entry_data.device_info.name - await reconnect_logic.start() - entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) + await reconnect_logic.start() + entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) - hass.async_create_task(complete_setup()) return True @@ -565,13 +562,11 @@ async def platform_async_setup_entry( """Update entities of this platform when entities are listed.""" old_infos = entry_data.info[component_key] new_infos: dict[int, EntityInfo] = {} - add_entities = [] + add_entities: list[_EntityT] = [] for info in infos: if not isinstance(info, info_type): # Filter out infos that don't belong to this platform. continue - # cast back to upper type, otherwise mypy gets confused - info = cast(EntityInfo, info) if info.key in old_infos: # Update existing entity diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index ecfa381bc69..9fd12634e43 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -25,6 +25,7 @@ from homeassistant.data_entry_flow import FlowResult from . import CONF_NOISE_PSK, DOMAIN, DomainData ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" +ESPHOME_URL = "https://esphome.io/" class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): @@ -55,7 +56,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = error return self.async_show_form( - step_id="user", data_schema=vol.Schema(fields), errors=errors + step_id="user", + data_schema=vol.Schema(fields), + errors=errors, + description_placeholders={"esphome_url": ESPHOME_URL}, ) async def async_step_user( diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 41a0e89245e..80fd855379e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -101,13 +101,8 @@ class RuntimeEntryData: ) -> None: async with self.platform_load_lock: needed = platforms - self.loaded_platforms - tasks = [] - for platform in needed: - tasks.append( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) - if tasks: - await asyncio.wait(tasks) + if needed: + await hass.config_entries.async_forward_entry_setups(entry, needed) self.loaded_platforms |= needed async def async_update_static_infos( diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index a8a76c2b0c8..cf748b27170 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.10.0"], + "requirements": ["aioesphomeapi==10.11.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 45a73a5e5af..897ab86b18a 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -67,6 +67,7 @@ _STATE_CLASSES: EsphomeEnumMapper[ EsphomeSensorStateClass.NONE: None, EsphomeSensorStateClass.MEASUREMENT: SensorStateClass.MEASUREMENT, EsphomeSensorStateClass.TOTAL_INCREASING: SensorStateClass.TOTAL_INCREASING, + EsphomeSensorStateClass.TOTAL: SensorStateClass.TOTAL, } ) diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 62814f2723b..b1b1ba94e3f 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -6,7 +6,7 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips", + "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address", "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_psk": "The transport encryption key is invalid. Please ensure it matches what you have in your configuration" @@ -17,7 +17,7 @@ "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" }, - "description": "Please enter connection settings of your [ESPHome](https://esphomelib.com/) node." + "description": "Please enter connection settings of your [ESPHome]({esphome_url}) node." }, "authenticate": { "data": { diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index 4c990994e47..ef9814d3ac8 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -9,7 +9,7 @@ "connection_error": "No s'ha pogut connectar amb ESP. Verifica que l'arxiu YAML cont\u00e9 la l\u00ednia 'api:'.", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_psk": "La clau de xifratge de transport \u00e9s inv\u00e0lida. Assegura't que coincideix amb la de la configuraci\u00f3", - "resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Amfitri\u00f3", "port": "Port" }, - "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu node [ESPHome](https://esphomelib.com/)." + "description": "Introdueix la configuraci\u00f3 de connexi\u00f3 del node [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 6229c09a03e..7556739ce93 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -9,7 +9,7 @@ "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_psk": "Der Transportverschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig. Bitte stelle sicher, dass es mit deiner Konfiguration \u00fcbereinstimmt", - "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege bitte eine statische IP-Adresse fest" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Port" }, - "description": "Bitte gib die Verbindungseinstellungen deines [ESPHome](https://esphomelib.com/)-Knotens ein." + "description": "Bitte gib die Verbindungseinstellungen deines [ESPHome]( {esphome_url} )-Knotens ein." } } } diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index 5ca5c03f8e9..b0b502631df 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -9,7 +9,7 @@ "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", "invalid_auth": "Invalid authentication", "invalid_psk": "The transport encryption key is invalid. Please ensure it matches what you have in your configuration", - "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Port" }, - "description": "Please enter connection settings of your [ESPHome](https://esphomelib.com/) node." + "description": "Please enter connection settings of your [ESPHome]({esphome_url}) node." } } } diff --git a/homeassistant/components/esphome/translations/et.json b/homeassistant/components/esphome/translations/et.json index ea5119b190d..29fa98bafb0 100644 --- a/homeassistant/components/esphome/translations/et.json +++ b/homeassistant/components/esphome/translations/et.json @@ -9,7 +9,7 @@ "connection_error": "ESP-ga ei saa \u00fchendust luua. Veendu, et YAML-fail sisaldab rida 'api:'.", "invalid_auth": "Tuvastamise viga", "invalid_psk": "\u00dclekande kr\u00fcpteerimisv\u00f5ti on kehtetu. Veendu, et see vastab seadetes sisalduvale", - "resolve_error": "ESP aadressi ei \u00f5nnestu lahendada. Kui see viga p\u00fcsib, m\u00e4\u00e4ra staatiline IP-aadress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "ESP aadressi ei saa lahendada. Kui see t\u00f5rge p\u00fcsib, m\u00e4\u00e4ra staatiline IP-aadress" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "", "port": "Port" }, - "description": "Sisesta oma [ESPHome](https://esphomelib.com/) s\u00f5lme \u00fchenduse s\u00e4tted." + "description": "Sisesta oma [ESPHome]({esphome_url}) s\u00f5lme \u00fchenduse s\u00e4tted." } } } diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 330c2823409..8ac9feb8a93 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -9,7 +9,7 @@ "connection_error": "Impossible de se connecter \u00e0 ESP. Assurez-vous que votre fichier YAML contient une ligne 'api:'.", "invalid_auth": "Authentification non valide", "invalid_psk": "La cl\u00e9 de chiffrement de transport n\u2019est pas valide. Assurez-vous qu\u2019elle correspond \u00e0 ce que vous avez dans votre configuration", - "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, veuillez d\u00e9finir une adresse IP statique: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, essayez de d\u00e9finir une adresse IP statique" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "H\u00f4te", "port": "Port" }, - "description": "Veuillez saisir les param\u00e8tres de connexion de votre n\u0153ud [ESPHome] (https://esphomelib.com/)." + "description": "Veuillez saisir les param\u00e8tres de connexion de votre n\u0153ud [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index e8e6c9b2dc2..1f98953678c 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -8,8 +8,8 @@ "error": { "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML konfigur\u00e1ci\u00f3 tartalmaz egy \"api:\" sort.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "invalid_psk": "Az adat\u00e1tviteli titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen. K\u00e9rj\u00fck, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy megegyezik a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151vel.", - "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rem, \u00e1ll\u00edtson be egy statikus IP-c\u00edmet: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "invalid_psk": "Az adat\u00e1tviteli titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy megegyezik a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151vel.", + "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rem, \u00e1ll\u00edtson be egy statikus IP-c\u00edmet." }, "flow_title": "{name}", "step": { @@ -27,20 +27,20 @@ "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151, {name} titkos\u00edt\u00e1si kulcs\u00e1t." + "description": "K\u00e9rem, adja meg a bekonfigur\u00e1lt titkos\u00edt\u00e1si kulcsot: {name}" }, "reauth_confirm": { "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "{name} ESPHome v\u00e9gpont aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rj\u00fck, adja meg az aktu\u00e1lis kulcsot." + "description": "{name} ESPHome v\u00e9gpont aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rem, adja meg az aktu\u00e1lis titkos\u00edt\u00e1si kulcsot." }, "user": { "data": { "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rem, adja meg az [ESPHome](https://esphomelib.com/) csom\u00f3pontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." + "description": "K\u00e9rem, adja meg az [ESPHome]({esphome_url}) csom\u00f3pontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." } } } diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index e099405bf0d..155bf33d986 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -9,7 +9,7 @@ "connection_error": "Tidak dapat terhubung ke ESP. Pastikan file YAML Anda mengandung baris 'api:'.", "invalid_auth": "Autentikasi tidak valid", "invalid_psk": "Kunci enkripsi transport tidak valid. Pastikan kuncinya sesuai dengan yang ada pada konfigurasi Anda", - "resolve_error": "Tidak dapat menemukan alamat ESP. Jika kesalahan ini terus terjadi, atur alamat IP statis: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Tidak dapat menemukan alamat ESP. Jika kesalahan ini terus terjadi, atur alamat IP statis" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Port" }, - "description": "Masukkan pengaturan koneksi node [ESPHome](https://esphomelib.com/)." + "description": "Masukkan pengaturan koneksi node [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index 62c75238378..b1c4e12d088 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -9,7 +9,7 @@ "connection_error": "Impossibile connettersi a ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", "invalid_auth": "Autenticazione non valida", "invalid_psk": "La chiave di cifratura del trasporto non \u00e8 valida. Assicurati che corrisponda a ci\u00f2 che hai nella tua configurazione", - "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se questo errore persiste, imposta un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se l'errore persiste, impostare un indirizzo IP statico" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Porta" }, - "description": "Inserisci le impostazioni di connessione del tuo nodo [ESPHome](https://esphomelib.com/)." + "description": "Inserisci le impostazioni di connessione del tuo nodo [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json index 0779d072b94..a4bfbc327dd 100644 --- a/homeassistant/components/esphome/translations/ja.json +++ b/homeassistant/components/esphome/translations/ja.json @@ -9,7 +9,7 @@ "connection_error": "ESP\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002YAML\u30d5\u30a1\u30a4\u30eb\u306b 'api:' \u306e\u884c\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_psk": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u306e\u6697\u53f7\u5316\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059\u3002\u8a2d\u5b9a\u3068\u4e00\u81f4\u3057\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "resolve_error": "ESP\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u9759\u7684\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "ESP\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001\u56fa\u5b9aIP\u30a2\u30c9\u30ec\u30b9\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 0d583893570..1fabb774aa2 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -9,7 +9,7 @@ "connection_error": "Kan ikke koble til ESP. Kontroller at YAML filen din inneholder en \"api:\" linje.", "invalid_auth": "Ugyldig godkjenning", "invalid_psk": "Transportkrypteringsn\u00f8kkelen er ugyldig. S\u00f8rg for at den samsvarer med det du har i konfigurasjonen", - "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, vennligst [sett en statisk IP-adresse](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)" + "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, vennligst angi en statisk IP-adresse" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Vert", "port": "Port" }, - "description": "Vennligst fyll inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node." + "description": "Vennligst skriv inn tilkoblingsinnstillingene for [ESPHome]( {esphome_url} )-noden." } } } diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index da6154e9fb6..dd495f0f097 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -9,7 +9,7 @@ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_psk": "Klucz szyfruj\u0105cy transport jest nieprawid\u0142owy. Upewnij si\u0119, \u017ce pasuje do tego, kt\u00f3ry masz w swojej konfiguracji.", - "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Nazwa hosta lub adres IP", "port": "Port" }, - "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia [ESPHome](https://esphomelib.com/) w\u0119z\u0142a." + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia w\u0119z\u0142a [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/pt-BR.json b/homeassistant/components/esphome/translations/pt-BR.json index 737bc5020af..21fd9067be1 100644 --- a/homeassistant/components/esphome/translations/pt-BR.json +++ b/homeassistant/components/esphome/translations/pt-BR.json @@ -9,7 +9,7 @@ "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_psk": "A chave de criptografia de transporte \u00e9 inv\u00e1lida. Certifique-se de que corresponde ao que voc\u00ea tem em sua configura\u00e7\u00e3o", - "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, por favor, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se o erro persistir, defina um endere\u00e7o IP est\u00e1tico" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Nome do host", "port": "Porta" }, - "description": "Por favor insira as configura\u00e7\u00f5es de conex\u00e3o de seu n\u00f3 de [ESPHome] (https://esphomelib.com/)." + "description": "Insira as configura\u00e7\u00f5es de conex\u00e3o do seu n\u00f3 [ESPHome]( {esphome_url} )." } } } diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index 60eeaa3f4b2..da3216186ea 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 8ba4a573cec..ea0a3105226 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -9,7 +9,7 @@ "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_psk": "\u041a\u043b\u044e\u0447 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043e\u043d \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0432 \u0412\u0430\u0448\u0435\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", - "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." + "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 [ESPHome](https://esphomelib.com/)." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index b2cede2b572..d4388dde8b8 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -9,7 +9,7 @@ "connection_error": "ESP'ye ba\u011flan\u0131lam\u0131yor. L\u00fctfen YAML dosyan\u0131z\u0131n bir 'api:' sat\u0131r\u0131 i\u00e7erdi\u011finden emin olun.", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_psk": "Aktar\u0131m \u015fifreleme anahtar\u0131 ge\u00e7ersiz. L\u00fctfen yap\u0131land\u0131rman\u0131zda sahip oldu\u011funuzla e\u015fle\u015fti\u011finden emin olun", - "resolve_error": "ESP'nin adresi \u00e7\u00f6z\u00fclemiyor. Bu hata devam ederse, l\u00fctfen statik bir IP adresi ayarlay\u0131n: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "ESP'nin adresi \u00e7\u00f6z\u00fclemiyor. Bu hata devam ederse, l\u00fctfen statik bir IP adresi ayarlay\u0131n" }, "flow_title": "{name}", "step": { @@ -21,7 +21,7 @@ }, "discovery_confirm": { "description": "ESPHome d\u00fc\u011f\u00fcm\u00fcn\u00fc ` {name} ` Home Assistant'a eklemek istiyor musunuz?", - "title": "Ke\u015ffedilen ESPHome d\u00fc\u011f\u00fcm\u00fc" + "title": "ESPHome d\u00fc\u011f\u00fcm\u00fc ke\u015ffedildi" }, "encryption_key": { "data": { @@ -40,7 +40,7 @@ "host": "Sunucu", "port": "Port" }, - "description": "L\u00fctfen [ESPHome](https://esphomelib.com/) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." + "description": "L\u00fctfen [ESPHome]( {esphome_url} ) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." } } } diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 976d0317faa..44f50a433e3 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -9,7 +9,7 @@ "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 ESP\uff0c\u8acb\u78ba\u5b9a\u60a8\u7684 YAML \u6a94\u6848\u5305\u542b\u300capi:\u300d\u8a2d\u5b9a\u5217\u3002", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_psk": "\u50b3\u8f38\u91d1\u9470\u7121\u6548\u3002\u8acb\u78ba\u5b9a\u8207\u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u91d1\u9470\u76f8\u7b26\u5408", - "resolve_error": "\u7121\u6cd5\u89e3\u6790 ESP \u4f4d\u5740\uff0c\u5047\u5982\u6b64\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u53c3\u8003\u8aaa\u660e\u8a2d\u5b9a\u70ba\u975c\u614b\u56fa\u5b9a IP \uff1a https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "\u7121\u6cd5\u89e3\u6790 ESP \u4f4d\u5740\uff0c\u5047\u5982\u6b64\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u53c3\u8003\u8aaa\u660e\u8a2d\u5b9a\u70ba\u975c\u614b\u56fa\u5b9a IP" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "description": "\u8acb\u8f38\u5165 [ESPHome](https://esphomelib.com/) \u7bc0\u9ede\u9023\u7dda\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 [ESPHome]({esphome_url}) \u7bc0\u9ede\u9023\u7dda\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index da3cbb51717..d7083715394 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/evil_genius_labs/translations/ca.json b/homeassistant/components/evil_genius_labs/translations/ca.json index aa6d7355314..21810a50f65 100644 --- a/homeassistant/components/evil_genius_labs/translations/ca.json +++ b/homeassistant/components/evil_genius_labs/translations/ca.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", + "timeout": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/pt.json b/homeassistant/components/evil_genius_labs/translations/pt.json new file mode 100644 index 00000000000..bf245f20e6e --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "timeout": "Tempo limite para estabelecer liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 908dff48aef..9c46ff43032 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, - TEMP_CELSIUS, Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -114,8 +113,9 @@ def _dt_aware_to_naive(dt_aware: dt) -> dt: def convert_until(status_dict: dict, until_key: str) -> None: """Reformat a dt str from "%Y-%m-%dT%H:%M:%SZ" as local/aware/isoformat.""" - if until_key in status_dict: # only present for certain modes - dt_utc_naive = dt_util.parse_datetime(status_dict[until_key]) + if until_key in status_dict and ( # only present for certain modes + dt_utc_naive := dt_util.parse_datetime(status_dict[until_key]) + ): status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() @@ -126,7 +126,9 @@ def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: """Convert a string to snake_case.""" string = re.sub(r"[\-\.\s]", "_", str(key)) return (string[0]).lower() + re.sub( - r"[A-Z]", lambda matched: f"_{matched.group(0).lower()}", string[1:] + r"[A-Z]", + lambda matched: f"_{matched.group(0).lower()}", # type:ignore[str-bytes-safe] + string[1:], ) return { @@ -137,7 +139,7 @@ def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: } -def _handle_exception(err) -> bool: +def _handle_exception(err) -> None: """Return False if the exception can't be ignored.""" try: raise err @@ -191,15 +193,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return ({}, None) # evohomeasync2 requires naive/local datetimes as strings - if tokens.get(ACCESS_TOKEN_EXPIRES) is not None: - tokens[ACCESS_TOKEN_EXPIRES] = _dt_aware_to_naive( - dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) - ) + if tokens.get(ACCESS_TOKEN_EXPIRES) is not None and ( + expires := dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) + ): + tokens[ACCESS_TOKEN_EXPIRES] = _dt_aware_to_naive(expires) user_data = tokens.pop(USER_DATA, None) return (tokens, user_data) - store = Store(hass, STORAGE_VER, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) tokens, user_data = await load_auth_tokens(store) client_v2 = evohomeasync2.EvohomeClient( @@ -231,7 +233,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return False if _LOGGER.isEnabledFor(logging.DEBUG): - _config = {"locationInfo": {"timeZone": None}, GWS: [{TCS: None}]} + _config: dict[str, Any] = { + "locationInfo": {"timeZone": None}, + GWS: [{TCS: None}], + } _config["locationInfo"]["timeZone"] = loc_config["locationInfo"]["timeZone"] _config[GWS][0][TCS] = loc_config[GWS][0][TCS] _LOGGER.debug("Config = %s", _config) @@ -390,7 +395,14 @@ def setup_service_functions(hass: HomeAssistant, broker): class EvoBroker: """Container for evohome client and data.""" - def __init__(self, hass, client, client_v1, store, params) -> None: + def __init__( + self, + hass, + client: evohomeasync2.EvohomeClient, + client_v1: evohomeasync.EvohomeClient | None, + store: Store[dict[str, Any]], + params, + ) -> None: """Initialize the evohome client and its data structure.""" self.hass = hass self.client = client @@ -404,7 +416,7 @@ class EvoBroker: self.tcs_utc_offset = timedelta( minutes=client.locations[loc_idx].timeZone[UTC_OFFSET] ) - self.temps = {} + self.temps: dict[str, Any] | None = {} async def save_auth_tokens(self) -> None: """Save access tokens and session IDs to the store for later use.""" @@ -444,6 +456,8 @@ class EvoBroker: async def _update_v1_api_temps(self, *args, **kwargs) -> None: """Get the latest high-precision temperatures of the default Location.""" + assert self.client_v1 + def get_session_id(client_v1) -> str | None: user_data = client_v1.user_data if client_v1 else None return user_data.get("sessionId") if user_data else None @@ -519,50 +533,36 @@ class EvoDevice(Entity): DHW controller. """ + _attr_should_poll = False + def __init__(self, evo_broker, evo_device) -> None: """Initialize the evohome entity.""" self._evo_device = evo_device self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs - self._unique_id = self._name = self._icon = self._precision = None - self._device_state_attrs = {} + self._device_state_attrs: dict[str, Any] = {} async def async_refresh(self, payload: dict | None = None) -> None: """Process any signals.""" if payload is None: self.async_schedule_update_ha_state(force_refresh=True) return - if payload["unique_id"] != self._unique_id: + if payload["unique_id"] != self._attr_unique_id: return if payload["service"] in (SVC_SET_ZONE_OVERRIDE, SVC_RESET_ZONE_OVERRIDE): await self.async_zone_svc_request(payload["service"], payload["data"]) return await self.async_tcs_svc_request(payload["service"], payload["data"]) - async def async_tcs_svc_request(self, service: dict, data: dict) -> None: + async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (system mode) for a controller.""" raise NotImplementedError - async def async_zone_svc_request(self, service: dict, data: dict) -> None: + async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (setpoint override) for a zone.""" raise NotImplementedError - @property - def should_poll(self) -> bool: - """Evohome entities should not be polled.""" - return False - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the evohome entity.""" - return self._name - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the evohome-specific state attributes.""" @@ -576,25 +576,10 @@ class EvoDevice(Entity): return {"status": convert_dict(status)} - @property - def icon(self) -> str: - """Return the icon to use in the frontend UI.""" - return self._icon - async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" async_dispatcher_connect(self.hass, DOMAIN, self.async_refresh) - @property - def precision(self) -> float: - """Return the temperature precision to use in the frontend UI.""" - return self._precision - - @property - def temperature_unit(self) -> str: - """Return the temperature unit to use in the frontend UI.""" - return TEMP_CELSIUS - class EvoChild(EvoDevice): """Base for any evohome child. @@ -605,8 +590,8 @@ class EvoChild(EvoDevice): def __init__(self, evo_broker, evo_device) -> None: """Initialize a evohome Controller (hub).""" super().__init__(evo_broker, evo_device) - self._schedule = {} - self._setpoints = {} + self._schedule: dict[str, Any] = {} + self._setpoints: dict[str, Any] = {} @property def current_temperature(self) -> float | None: @@ -620,6 +605,8 @@ class EvoChild(EvoDevice): if self._evo_device.temperatureStatus["isAvailable"]: return self._evo_device.temperatureStatus["temperature"] + return None + @property def setpoints(self) -> dict[str, Any]: """Return the current/next setpoints from the schedule. @@ -660,9 +647,12 @@ class EvoChild(EvoDevice): day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] switchpoint = day["Switchpoints"][idx] + switchpoint_time_of_day = dt_util.parse_datetime( + f"{sp_date}T{switchpoint['TimeOfDay']}" + ) + assert switchpoint_time_of_day dt_aware = _dt_evo_to_aware( - dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}"), - self._evo_broker.tcs_utc_offset, + switchpoint_time_of_day, self._evo_broker.tcs_utc_offset ) self._setpoints[f"{key}_sp_from"] = dt_aware.isoformat() @@ -691,7 +681,8 @@ class EvoChild(EvoDevice): async def async_update(self) -> None: """Get the latest state data.""" next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") - if dt_util.now() >= dt_util.parse_datetime(next_sp_from): + next_sp_from_dt = dt_util.parse_datetime(next_sp_from) + if next_sp_from_dt is None or dt_util.now() >= next_sp_from_dt: await self._update_schedule() # no schedule, or it's out-of-date self._device_state_attrs = {"setpoints": self.setpoints} diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 676bc88f470..c1a630d0d05 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime as dt import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -13,8 +14,9 @@ from homeassistant.components.climate.const import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.const import PRECISION_TENTHS +from homeassistant.const import PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -93,9 +95,8 @@ async def async_setup_platform( broker.params[CONF_LOCATION_IDX], ) - controller = EvoController(broker, broker.tcs) + entities: list[EvoClimateEntity] = [EvoController(broker, broker.tcs)] - zones = [] for zone in broker.tcs.zones.values(): if zone.modelType == "HeatingZone" or zone.zoneType == "Thermostat": _LOGGER.debug( @@ -107,7 +108,7 @@ async def async_setup_platform( ) new_entity = EvoZone(broker, zone) - zones.append(new_entity) + entities.append(new_entity) else: _LOGGER.warning( @@ -119,12 +120,14 @@ async def async_setup_platform( zone.name, ) - async_add_entities([controller] + zones, update_before_add=True) + async_add_entities(entities, update_before_add=True) class EvoClimateEntity(EvoDevice, ClimateEntity): """Base for an evohome Climate device.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Climate device.""" super().__init__(evo_broker, evo_device) @@ -151,24 +154,24 @@ class EvoZone(EvoChild, EvoClimateEntity): if evo_device.modelType.startswith("VisionProWifi"): # this system does not have a distinct ID for the zone - self._unique_id = f"{evo_device.zoneId}z" + self._attr_unique_id = f"{evo_device.zoneId}z" else: - self._unique_id = evo_device.zoneId + self._attr_unique_id = evo_device.zoneId - self._name = evo_device.name - self._icon = "mdi:radiator" + self._attr_name = evo_device.name if evo_broker.client_v1: - self._precision = PRECISION_TENTHS + self._attr_precision = PRECISION_TENTHS else: - self._precision = self._evo_device.setpointCapabilities["valueResolution"] + self._attr_precision = self._evo_device.setpointCapabilities[ + "valueResolution" + ] - self._preset_modes = list(HA_PRESET_TO_EVO) self._attr_supported_features = ( ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) - async def async_zone_svc_request(self, service: dict, data: dict) -> None: + async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (setpoint override) for a zone.""" if service == SVC_RESET_ZONE_OVERRIDE: await self._evo_broker.call_client_api( @@ -272,7 +275,7 @@ class EvoZone(EvoChild, EvoClimateEntity): self._evo_device.cancel_temp_override() ) - async def async_set_preset_mode(self, preset_mode: str | None) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode; if None, then revert to following the schedule.""" evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW) @@ -313,25 +316,25 @@ class EvoController(EvoClimateEntity): It is assumed there is only one TCS per location, and they are thus synonymous. """ + _attr_icon = "mdi:thermostat" + _attr_precision = PRECISION_TENTHS + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Controller/Location.""" super().__init__(evo_broker, evo_device) - self._unique_id = evo_device.systemId - self._name = evo_device.location.name - self._icon = "mdi:thermostat" - - self._precision = PRECISION_TENTHS + self._attr_unique_id = evo_device.systemId + self._attr_name = evo_device.location.name modes = [m["systemMode"] for m in evo_broker.config["allowedSystemModes"]] - self._preset_modes = [ + self._attr_preset_modes = [ TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA) ] self._attr_supported_features = ( - ClimateEntityFeature.PRESET_MODE if self._preset_modes else 0 + ClimateEntityFeature.PRESET_MODE if self._attr_preset_modes else 0 ) - async def async_tcs_svc_request(self, service: dict, data: dict) -> None: + async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (system mode) for a controller. Data validation is not required, it will have been done upstream. @@ -384,25 +387,17 @@ class EvoController(EvoClimateEntity): """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - @property - def min_temp(self) -> float: - """Return None as Controllers don't have a target temperature.""" - return None - - @property - def max_temp(self) -> float: - """Return None as Controllers don't have a target temperature.""" - return None - async def async_set_temperature(self, **kwargs) -> None: """Raise exception as Controllers don't have a target temperature.""" raise NotImplementedError("Evohome Controllers don't have target temperatures.") - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set an operating mode for a Controller.""" - await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) + if not (tcs_mode := HA_HVAC_TO_TCS.get(hvac_mode)): + raise HomeAssistantError(f"Invalid hvac_mode: {hvac_mode}") + await self._set_tcs_mode(tcs_mode) - async def async_set_preset_mode(self, preset_mode: str | None) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode; if None, then revert to 'Auto' mode.""" await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index f216862b232..ff54cfbe4a6 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -7,7 +7,13 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.const import ( + PRECISION_TENTHS, + PRECISION_WHOLE, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -51,15 +57,20 @@ async def async_setup_platform( class EvoDHW(EvoChild, WaterHeaterEntity): """Base for a Honeywell TCC DHW controller (aka boiler).""" + _attr_name = "DHW controller" + _attr_icon = "mdi:thermometer-lines" + _attr_operation_list = list(HA_STATE_TO_EVO) + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize an evohome DHW controller.""" super().__init__(evo_broker, evo_device) - self._unique_id = evo_device.dhwId - self._name = "DHW controller" - self._icon = "mdi:thermometer-lines" + self._attr_unique_id = evo_device.dhwId - self._precision = PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE + self._attr_precision = ( + PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE + ) self._attr_supported_features = ( WaterHeaterEntityFeature.AWAY_MODE | WaterHeaterEntityFeature.OPERATION_MODE ) @@ -71,11 +82,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity): return STATE_AUTO return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] - @property - def operation_list(self) -> list[str]: - """Return the list of available operations.""" - return list(HA_STATE_TO_EVO) - @property def is_away_mode_on(self): """Return True if away mode is on.""" diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 9d6f7864b84..51931f4b104 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -83,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, DATA_UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ezviz/translations/bg.json b/homeassistant/components/ezviz/translations/bg.json index 702e3b80001..7e54efd88a9 100644 --- a/homeassistant/components/ezviz/translations/bg.json +++ b/homeassistant/components/ezviz/translations/bg.json @@ -1,7 +1,15 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{serial}", "step": { + "confirm": { + "data": { + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "user": { "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Ezviz Cloud" } diff --git a/homeassistant/components/ezviz/translations/hu.json b/homeassistant/components/ezviz/translations/hu.json index 5907f66ceb4..49cd4b43d08 100644 --- a/homeassistant/components/ezviz/translations/hu.json +++ b/homeassistant/components/ezviz/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "ezviz_cloud_account_missing": "Ezviz cloud fi\u00f3k hi\u00e1nyzik. K\u00e9rj\u00fck, konfigur\u00e1lja \u00fajra az Ezviz cloud fi\u00f3kot.", + "ezviz_cloud_account_missing": "Ezviz cloud fi\u00f3k hi\u00e1nyzik. K\u00e9rem, konfigur\u00e1lja \u00fajra az Ezviz cloud fi\u00f3kot.", "unknown": "V\u00e1ratlan hiba" }, "error": { diff --git a/homeassistant/components/ezviz/translations/pt.json b/homeassistant/components/ezviz/translations/pt.json new file mode 100644 index 00000000000..b7e8b2d8b81 --- /dev/null +++ b/homeassistant/components/ezviz/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "Palavra-passe" + } + }, + "user_custom_url": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 8bfcf60f30a..10ddb13c228 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/faa_delays/translations/bg.json b/homeassistant/components/faa_delays/translations/bg.json index 93fa3f04d6c..ac15641f743 100644 --- a/homeassistant/components/faa_delays/translations/bg.json +++ b/homeassistant/components/faa_delays/translations/bg.json @@ -4,6 +4,7 @@ "already_configured": "\u0422\u043e\u0432\u0430 \u043b\u0435\u0442\u0438\u0449\u0435 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e." }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_airport": "\u041a\u043e\u0434\u044a\u0442 \u043d\u0430 \u043b\u0435\u0442\u0438\u0449\u0435\u0442\u043e \u043d\u0435 \u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index 1a9bb05e140..9a50a905922 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -2,7 +2,7 @@ "domain": "feedreader", "name": "Feedreader", "documentation": "https://www.home-assistant.io/integrations/feedreader", - "requirements": ["feedparser==6.0.2"], + "requirements": ["feedparser==6.0.10"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["feedparser", "sgmllib3k"] diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index bd7c3a09ec0..9431cd162bc 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -29,8 +29,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -156,16 +157,21 @@ class FibaroController: ) # List of devices by entity platform self._callbacks: dict[Any, Any] = {} # Update value callbacks by deviceId self._state_handler = None # Fiblary's StateHandler object - self.hub_serial = None # Unique serial number of the hub - self.name = None # The friendly name of the hub + self.hub_serial: str # Unique serial number of the hub + self.hub_name: str # The friendly name of the hub + self.hub_software_version: str + self.hub_api_url: str = config[CONF_URL] + # Device infos by fibaro device id + self._device_infos: dict[int, DeviceInfo] = {} def connect(self): """Start the communication with the Fibaro controller.""" try: login = self._client.login.get() info = self._client.info.get() - self.hub_serial = slugify(info.serialNumber) - self.name = slugify(info.hcName) + self.hub_serial = info.serialNumber + self.hub_name = info.hcName + self.hub_software_version = info.softVersion except AssertionError: _LOGGER.error("Can't connect to Fibaro HC. Please check URL") return False @@ -305,6 +311,44 @@ class FibaroController: platform = Platform.LIGHT return platform + def _create_device_info(self, device: Any, devices: list) -> None: + """Create the device info. Unrooted entities are directly shown below the home center.""" + + # The home center is always id 1 (z-wave primary controller) + if "parentId" not in device or device.parentId <= 1: + return + + master_entity: Any | None = None + if device.parentId == 1: + master_entity = device + else: + for parent in devices: + if "id" in parent and parent.id == device.parentId: + master_entity = parent + if master_entity is None: + _LOGGER.error("Parent with id %s not found", device.parentId) + return + + if "zwaveCompany" in master_entity.properties: + manufacturer = master_entity.properties.zwaveCompany + else: + manufacturer = "Unknown" + + self._device_infos[master_entity.id] = DeviceInfo( + identifiers={(DOMAIN, master_entity.id)}, + manufacturer=manufacturer, + name=master_entity.name, + via_device=(DOMAIN, self.hub_serial), + ) + + def get_device_info(self, device: Any) -> DeviceInfo: + """Get the device info by fibaro device id.""" + if device.id in self._device_infos: + return self._device_infos[device.id] + if "parentId" in device and device.parentId in self._device_infos: + return self._device_infos[device.parentId] + return DeviceInfo(identifiers={(DOMAIN, self.hub_serial)}) + def _read_scenes(self): scenes = self._client.scenes.list() self._scene_map = {} @@ -321,14 +365,14 @@ class FibaroController: device.ha_id = ( f"scene_{slugify(room_name)}_{slugify(device.name)}_{device.id}" ) - device.unique_id_str = f"{self.hub_serial}.scene.{device.id}" + device.unique_id_str = f"{slugify(self.hub_serial)}.scene.{device.id}" self._scene_map[device.id] = device self.fibaro_devices[Platform.SCENE].append(device) _LOGGER.debug("%s scene -> %s", device.ha_id, device) def _read_devices(self): """Read and process the device list.""" - devices = self._client.devices.list() + devices = list(self._client.devices.list()) self._device_map = {} last_climate_parent = None last_endpoint = None @@ -355,7 +399,8 @@ class FibaroController: device.mapped_platform = None if (platform := device.mapped_platform) is None: continue - device.unique_id_str = f"{self.hub_serial}.{device.id}" + device.unique_id_str = f"{slugify(self.hub_serial)}.{device.id}" + self._create_device_info(device, devices) self._device_map[device.id] = device _LOGGER.debug( "%s (%s, %s) -> %s %s", @@ -462,7 +507,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for platform in PLATFORMS: devices[platform] = [*controller.fibaro_devices[platform]] - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # register the hub device info separately as the hub has sometimes no entities + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, controller.hub_serial)}, + manufacturer="Fibaro", + name=controller.hub_name, + model=controller.hub_serial, + sw_version=controller.hub_software_version, + configuration_url=controller.hub_api_url.removesuffix("/api/"), + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) controller.enable_state_handler() @@ -490,6 +547,7 @@ class FibaroDevice(Entity): self.ha_id = fibaro_device.ha_id self._attr_name = fibaro_device.friendly_name self._attr_unique_id = fibaro_device.unique_id_str + self._attr_device_info = self.controller.get_device_info(fibaro_device) # propagate hidden attribute set in fibaro home center to HA if "visible" in fibaro_device and fibaro_device.visible is False: self._attr_entity_registry_visible_default = False diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index b0ea05e49e1..fd53bd5b94f 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any +from slugify import slugify import voluptuous as vol from homeassistant import config_entries @@ -44,9 +45,12 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str _LOGGER.debug( "Successfully connected to fibaro home center %s with name %s", controller.hub_serial, - controller.name, + controller.hub_name, ) - return {"serial_number": controller.hub_serial, "name": controller.name} + return { + "serial_number": slugify(controller.hub_serial), + "name": controller.hub_name, + } class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index e4e8b19d308..045adce5764 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -7,6 +7,7 @@ from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FIBARO_DEVICES, FibaroDevice @@ -33,6 +34,15 @@ async def async_setup_entry( class FibaroScene(FibaroDevice, Scene): """Representation of a Fibaro scene entity.""" + def __init__(self, fibaro_device: Any) -> None: + """Initialize the Fibaro scene.""" + super().__init__(fibaro_device) + + # All scenes are shown on hub device + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.controller.hub_serial)} + ) + def activate(self, **kwargs: Any) -> None: """Activate the scene.""" self.fibaro_device.start() diff --git a/homeassistant/components/fibaro/translations/cs.json b/homeassistant/components/fibaro/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/fibaro/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/pt.json b/homeassistant/components/fibaro/translations/pt.json new file mode 100644 index 00000000000..db0e0c2a137 --- /dev/null +++ b/homeassistant/components/fibaro/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 61ef26f0a66..9e08615d4ab 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.config.is_allowed_path(path): raise ConfigEntryNotReady(f"Filepath {path} is not valid or allowed") - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 5f66aefaab5..06e567f14cb 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -119,6 +119,7 @@ class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): """Filesize sensor.""" entity_description: SensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -130,7 +131,7 @@ class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): """Initialize the Filesize sensor.""" super().__init__(coordinator) base_name = path.split("/")[-1] - self._attr_name = f"{base_name} {description.name}" + self._attr_name = description.name self._attr_unique_id = ( entry_id if description.key == "file" else f"{entry_id}-{description.key}" ) diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index ffd82307940..a9a4323fe12 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index ffc4d39a89b..a58cd0591d1 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -197,11 +197,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b sw_version=board.firmware_version, ) - for (conf, platform) in CONF_PLATFORM_MAP.items(): - if conf in config_entry.data: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + await hass.config_entries.async_forward_entry_setups( + config_entry, + [ + platform + for conf, platform in CONF_PLATFORM_MAP.items() + if conf in config_entry.data + ], + ) return True diff --git a/homeassistant/components/firmata/translations/pt.json b/homeassistant/components/firmata/translations/pt.json index 785f8887678..32a5bb07569 100644 --- a/homeassistant/components/firmata/translations/pt.json +++ b/homeassistant/components/firmata/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "N\u0101o foi poss\u00edvel conectar ao board do Firmata durante a configura\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { "one": "Um", diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 48b05482dec..3165843a23d 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -25,7 +25,7 @@ from homeassistant.const import ( CONF_CLIENT_SECRET, CONF_UNIT_SYSTEM, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level @@ -279,7 +279,6 @@ class FitbitAuthCallbackView(HomeAssistantView): self.add_entities = add_entities self.oauth = oauth - @callback async def get(self, request: Request) -> str: """Finish OAuth callback request.""" hass: HomeAssistant = request.app["hass"] diff --git a/homeassistant/components/fivem/__init__.py b/homeassistant/components/fivem/__init__.py index 1fe5ccf0b8f..7b0ae2e2758 100644 --- a/homeassistant/components/fivem/__init__.py +++ b/homeassistant/components/fivem/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fivem/translations/hu.json b/homeassistant/components/fivem/translations/hu.json index d29d0c9a802..28068946c16 100644 --- a/homeassistant/components/fivem/translations/hu.json +++ b/homeassistant/components/fivem/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l is, hogy a leg\u00fajabb FiveM szervert futtatja.", + "cannot_connect": "Nem siker\u00fclt csatlakozni. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l is, hogy a leg\u00fajabb FiveM szervert futtatja.", "invalid_game_name": "A j\u00e1t\u00e9k API-ja, amelyhez csatlakozni pr\u00f3b\u00e1l, nem FiveM.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/fivem/translations/pt.json b/homeassistant/components/fivem/translations/pt.json new file mode 100644 index 00000000000..cf73866f4a0 --- /dev/null +++ b/homeassistant/components/fivem/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown_error": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "name": "Nome", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 85e95db5513..fbd2f13d2b4 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -6,14 +6,19 @@ from dataclasses import dataclass from datetime import timedelta import logging -from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData -from fjaraskupan import Device, State, device_filter +from fjaraskupan import Device, State +from homeassistant.components.bluetooth import ( + BluetoothCallbackMatcher, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_address_present, + async_register_callback, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -66,16 +71,18 @@ class Coordinator(DataUpdateCoordinator[State]): async def _async_update_data(self) -> State: """Handle an explicit update request.""" if self._refresh_was_scheduled: - raise UpdateFailed("No data received within schedule.") + if async_address_present(self.hass, self.device.address): + return self.device.state + raise UpdateFailed( + "No data received within schedule, and device is no longer present" + ) await self.device.update() return self.device.state - def detection_callback( - self, ble_device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: + def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None: """Handle a new announcement of data.""" - self.device.detection_callback(ble_device, advertisement_data) + self.device.detection_callback(service_info.device, service_info.advertisement) self.async_set_updated_data(self.device.state) @@ -83,59 +90,52 @@ class Coordinator(DataUpdateCoordinator[State]): class EntryState: """Store state of config entry.""" - scanner: BleakScanner coordinators: dict[str, Coordinator] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"DuplicateData": True}) - - state = EntryState(scanner, {}) + state = EntryState({}) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = state - async def detection_callback( - ble_device: BLEDevice, advertisement_data: AdvertisementData + def detection_callback( + service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: - if data := state.coordinators.get(ble_device.address): - _LOGGER.debug( - "Update: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - data.detection_callback(ble_device, advertisement_data) + if change != BluetoothChange.ADVERTISEMENT: + return + if data := state.coordinators.get(service_info.address): + _LOGGER.debug("Update: %s", service_info) + data.detection_callback(service_info) else: - if not device_filter(ble_device, advertisement_data): - return + _LOGGER.debug("Detected: %s", service_info) - _LOGGER.debug( - "Detected: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - device = Device(ble_device) + device = Device(service_info.device) device_info = DeviceInfo( - identifiers={(DOMAIN, ble_device.address)}, + identifiers={(DOMAIN, service_info.address)}, manufacturer="Fjäråskupan", name="Fjäråskupan", ) coordinator: Coordinator = Coordinator(hass, device, device_info) - coordinator.detection_callback(ble_device, advertisement_data) + coordinator.detection_callback(service_info) - state.coordinators[ble_device.address] = coordinator + state.coordinators[service_info.address] = coordinator async_dispatcher_send( hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator ) - scanner.register_detection_callback(detection_callback) - await scanner.start() - - async def on_hass_stop(event: Event) -> None: - await scanner.stop() - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + async_register_callback( + hass, + detection_callback, + BluetoothCallbackMatcher( + manufacturer_id=20296, + manufacturer_data_start=[79, 68, 70, 74, 65, 82], + ), + BluetoothScanningMode.ACTIVE, + ) ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -173,7 +173,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - entry_state: EntryState = hass.data[DOMAIN].pop(entry.entry_id) - await entry_state.scanner.stop() + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/fjaraskupan/binary_sensor.py b/homeassistant/components/fjaraskupan/binary_sensor.py index ef4f64c5ecd..0ea5c1669db 100644 --- a/homeassistant/components/fjaraskupan/binary_sensor.py +++ b/homeassistant/components/fjaraskupan/binary_sensor.py @@ -30,13 +30,13 @@ class EntityDescription(BinarySensorEntityDescription): SENSORS = ( EntityDescription( key="grease-filter", - name="Grease Filter", + name="Grease filter", device_class=BinarySensorDeviceClass.PROBLEM, is_on=lambda state: state.grease_filter_full, ), EntityDescription( key="carbon-filter", - name="Carbon Filter", + name="Carbon filter", device_class=BinarySensorDeviceClass.PROBLEM, is_on=lambda state: state.carbon_filter_full, ), @@ -68,6 +68,7 @@ class BinarySensor(CoordinatorEntity[Coordinator], BinarySensorEntity): """Grease filter sensor.""" entity_description: EntityDescription + _attr_has_entity_name = True def __init__( self, @@ -82,7 +83,6 @@ class BinarySensor(CoordinatorEntity[Coordinator], BinarySensorEntity): self._attr_unique_id = f"{device.address}-{entity_description.key}" self._attr_device_info = device_info - self._attr_name = f"{device_info['name']} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index ffac366500b..dd1dc03d3ad 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -1,42 +1,25 @@ """Config flow for Fjäråskupan integration.""" from __future__ import annotations -import asyncio - -import async_timeout -from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from fjaraskupan import device_filter +from homeassistant.components.bluetooth import async_discovered_service_info from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow from .const import DOMAIN -CONST_WAIT_TIME = 5.0 - async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" - event = asyncio.Event() + service_infos = async_discovered_service_info(hass) - def detection(device: BLEDevice, advertisement_data: AdvertisementData): - if device_filter(device, advertisement_data): - event.set() + for service_info in service_infos: + if device_filter(service_info.device, service_info.advertisement): + return True - async with BleakScanner( - detection_callback=detection, - filters={"DuplicateData": True}, - ): - try: - async with async_timeout.timeout(CONST_WAIT_TIME): - await event.wait() - except asyncio.TimeoutError: - return False - - return True + return False register_discovery_flow(DOMAIN, "Fjäråskupan", _async_has_devices) diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index e372d540f54..3642438d5d6 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -67,6 +67,7 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): """Fan entity.""" _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + _attr_has_entity_name = True def __init__( self, @@ -78,7 +79,6 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): super().__init__(coordinator) self._device = device self._default_on_speed = 25 - self._attr_name = device_info["name"] self._attr_unique_id = device.address self._attr_device_info = device_info self._percentage = 0 diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index 9d52c5cac82..f492f628a3a 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -29,6 +29,8 @@ async def async_setup_entry( class Light(CoordinatorEntity[Coordinator], LightEntity): """Light device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: Coordinator, @@ -42,7 +44,6 @@ class Light(CoordinatorEntity[Coordinator], LightEntity): self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_unique_id = device.address self._attr_device_info = device_info - self._attr_name = device_info["name"] async def async_turn_on(self, **kwargs): """Turn the light on.""" diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index 3ff6e599a6b..bf7956d297d 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -6,5 +6,12 @@ "requirements": ["fjaraskupan==1.0.2"], "codeowners": ["@elupus"], "iot_class": "local_polling", - "loggers": ["bleak", "fjaraskupan"] + "loggers": ["bleak", "fjaraskupan"], + "dependencies": ["bluetooth"], + "bluetooth": [ + { + "manufacturer_id": 20296, + "manufacturer_data_start": [79, 68, 70, 74, 65, 82] + } + ] } diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index 511d97cbed8..fb793d2328e 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -34,6 +34,8 @@ async def async_setup_entry( class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): """Periodic Venting.""" + _attr_has_entity_name = True + _attr_native_max_value: float = 59 _attr_native_min_value: float = 0 _attr_native_step: float = 1 @@ -51,7 +53,7 @@ class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): self._device = device self._attr_unique_id = f"{device.address}-periodic-venting" self._attr_device_info = device_info - self._attr_name = f"{device_info['name']} Periodic Venting" + self._attr_name = "Periodic venting" @property def native_value(self) -> float | None: diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index e4dbffab38e..81b499661b1 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -35,6 +35,8 @@ async def async_setup_entry( class RssiSensor(CoordinatorEntity[Coordinator], SensorEntity): """Sensor device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: Coordinator, @@ -45,7 +47,7 @@ class RssiSensor(CoordinatorEntity[Coordinator], SensorEntity): super().__init__(coordinator) self._attr_unique_id = f"{device.address}-signal-strength" self._attr_device_info = device_info - self._attr_name = f"{device_info['name']} Signal Strength" + self._attr_name = "Signal strength" self._attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT diff --git a/homeassistant/components/fjaraskupan/translations/ja.json b/homeassistant/components/fjaraskupan/translations/ja.json index f22b3c04c84..e7c39f8d142 100644 --- a/homeassistant/components/fjaraskupan/translations/ja.json +++ b/homeassistant/components/fjaraskupan/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 54eaf5a6917..a963d199c5a 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = FlickAPI(auth) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 3281410ec2d..3f9f70e294c 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flipr/translations/pt.json b/homeassistant/components/flipr/translations/pt.json new file mode 100644 index 00000000000..12a3977fdde --- /dev/null +++ b/homeassistant/components/flipr/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 2dcca979acc..b30e31de361 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: tasks = [device.async_refresh() for device in devices] await asyncio.gather(*tasks) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py index c395e6136fe..f5c051d1a70 100644 --- a/homeassistant/components/flo/binary_sensor.py +++ b/homeassistant/components/flo/binary_sensor.py @@ -48,7 +48,7 @@ class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): def __init__(self, device): """Initialize the pending alerts binary sensor.""" - super().__init__("pending_system_alerts", "Pending System Alerts", device) + super().__init__("pending_system_alerts", "Pending system alerts", device) @property def is_on(self): @@ -74,7 +74,7 @@ class FloWaterDetectedBinarySensor(FloEntity, BinarySensorEntity): def __init__(self, device): """Initialize the pending alerts binary sensor.""" - super().__init__("water_detected", "Water Detected", device) + super().__init__("water_detected", "Water detected", device) @property def is_on(self): diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 280f19dc57e..39ad57f5c03 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -14,6 +14,7 @@ class FloEntity(Entity): """A base class for Flo entities.""" _attr_force_update = False + _attr_has_entity_name = True _attr_should_poll = False def __init__( @@ -38,7 +39,7 @@ class FloEntity(Entity): identifiers={(FLO_DOMAIN, self._device.id)}, manufacturer=self._device.manufacturer, model=self._device.model, - name=self._device.device_name, + name=self._device.device_name.capitalize(), sw_version=self._device.firmware_version, ) diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index a5d54386633..e7fbd293bd1 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -22,12 +22,12 @@ from .entity import FloEntity WATER_ICON = "mdi:water" GAUGE_ICON = "mdi:gauge" -NAME_DAILY_USAGE = "Today's Water Usage" -NAME_CURRENT_SYSTEM_MODE = "Current System Mode" -NAME_FLOW_RATE = "Water Flow Rate" -NAME_WATER_TEMPERATURE = "Water Temperature" +NAME_DAILY_USAGE = "Today's water usage" +NAME_CURRENT_SYSTEM_MODE = "Current system mode" +NAME_FLOW_RATE = "Water flow rate" +NAME_WATER_TEMPERATURE = "Water temperature" NAME_AIR_TEMPERATURE = "Temperature" -NAME_WATER_PRESSURE = "Water Pressure" +NAME_WATER_PRESSURE = "Water pressure" NAME_HUMIDITY = "Humidity" NAME_BATTERY = "Battery" diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index 01ab2c9259e..884b76fc64e 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -68,7 +68,7 @@ class FloSwitch(FloEntity, SwitchEntity): def __init__(self, device: FloDeviceDataUpdateCoordinator) -> None: """Initialize the Flo switch.""" - super().__init__("shutoff_valve", "Shutoff Valve", device) + super().__init__("shutoff_valve", "Shutoff valve", device) self._state = self._device.last_known_valve_state == "open" @property diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 3ca99a335f2..294f50c50e2 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: FLUME_HTTP_SESSION: http_session, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 9a2ef2d4465..5e48e1561b5 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 8a232d3d103..f666e7412eb 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -42,12 +42,12 @@ SENSOR_TYPE_USER_TOTAL = "total" CDC_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_CDC_LEVEL, - name="CDC Level", + name="CDC level", icon="mdi:biohazard", ), SensorEntityDescription( key=SENSOR_TYPE_CDC_LEVEL2, - name="CDC Level 2", + name="CDC level 2", icon="mdi:biohazard", ), ) @@ -55,49 +55,49 @@ CDC_SENSOR_DESCRIPTIONS = ( USER_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_USER_CHICK, - name="Avian Flu Symptoms", + name="Avian flu symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_DENGUE, - name="Dengue Fever Symptoms", + name="Dengue fever symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_FLU, - name="Flu Symptoms", + name="Flu symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_LEPTO, - name="Leptospirosis Symptoms", + name="Leptospirosis symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_NO_SYMPTOMS, - name="No Symptoms", + name="No symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_SYMPTOMS, - name="Flu-like Symptoms", + name="Flu-like symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_TOTAL, - name="Total Symptoms", + name="Total symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, @@ -133,6 +133,8 @@ async def async_setup_entry( class FluNearYouSensor(CoordinatorEntity, SensorEntity): """Define a base Flu Near You sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index e6c1393154a..0284bf90ba0 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -187,7 +187,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = FluxLedUpdateCoordinator(hass, device, entry) hass.data[DOMAIN][entry.entry_id] = coordinator platforms = PLATFORMS_BY_TYPE[device.device_type] - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) async def _async_sync_time(*args: Any) -> None: """Set the time every morning at 02:40:30.""" diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 65c8a955dcf..18237c97e94 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -2,8 +2,9 @@ from __future__ import annotations from abc import abstractmethod +from collections.abc import Coroutine import logging -from typing import cast +from typing import Any, cast from flux_led.protocol import ( MUSIC_PIXELS_MAX, @@ -143,7 +144,7 @@ class FluxConfigNumber( ) -> None: """Initialize the flux number.""" super().__init__(coordinator, base_unique_id, name, key) - self._debouncer: Debouncer | None = None + self._debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None self._pending_value: int | None = None async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/flux_led/translations/pt.json b/homeassistant/components/flux_led/translations/pt.json new file mode 100644 index 00000000000..7f2f103180a --- /dev/null +++ b/homeassistant/components/flux_led/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "flow_title": "", + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 18d542c1d3b..ece451a9b0a 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index d9742cf5dfc..185025fb5ce 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -19,31 +19,31 @@ CONF_INVERTER_SIZE = "inverter_size" SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( key="energy_production_today", - name="Estimated Energy Production - Today", + name="Estimated energy production - today", state=lambda estimate: estimate.energy_production_today / 1000, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="energy_production_tomorrow", - name="Estimated Energy Production - Tomorrow", + name="Estimated energy production - tomorrow", state=lambda estimate: estimate.energy_production_tomorrow / 1000, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_today", - name="Highest Power Peak Time - Today", + name="Highest power peak time - today", device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_tomorrow", - name="Highest Power Peak Time - Tomorrow", + name="Highest power peak time - tomorrow", device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_production_now", - name="Estimated Power Production - Now", + name="Estimated power production - now", device_class=SensorDeviceClass.POWER, state=lambda estimate: estimate.power_production_now, state_class=SensorStateClass.MEASUREMENT, @@ -54,7 +54,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=1) ), - name="Estimated Power Production - Next Hour", + name="Estimated power production - next hour", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, @@ -64,7 +64,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=12) ), - name="Estimated Power Production - Next 12 Hours", + name="Estimated power production - next 12 hours", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, @@ -74,14 +74,14 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=24) ), - name="Estimated Power Production - Next 24 Hours", + name="Estimated power production - next 24 hours", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="energy_current_hour", - name="Estimated Energy Production - This Hour", + name="Estimated energy production - this hour", state=lambda estimate: estimate.energy_current_hour / 1000, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, @@ -89,7 +89,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( key="energy_next_hour", state=lambda estimate: estimate.sum_energy_production(1) / 1000, - name="Estimated Energy Production - Next Hour", + name="Estimated energy production - next hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 78335292a78..7bac69b1b6e 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -39,6 +39,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): """Defines a Forecast.Solar sensor.""" entity_description: ForecastSolarSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -58,7 +59,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, entry_id)}, manufacturer="Forecast.Solar", model=coordinator.data.account_type.value, - name="Solar Production Forecast", + name="Solar production forecast", configuration_url="https://forecast.solar", ) diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json index 33a69ad2fd7..3aac09afe1b 100644 --- a/homeassistant/components/forecast_solar/translations/hu.json +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -10,7 +10,7 @@ "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)", "name": "Elnevez\u00e9s" }, - "description": "T\u00f6ltse ki a napelemek adatait. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." + "description": "T\u00f6ltse ki a napelemek adatait. K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." } } }, @@ -25,7 +25,7 @@ "inverter_size": "Inverter m\u00e9rete (Watt)", "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)" }, - "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." + "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 kit\u00f6lt\u00e9se nem egy\u00e9rtelm\u0171." } } } diff --git a/homeassistant/components/forecast_solar/translations/pt.json b/homeassistant/components/forecast_solar/translations/pt.json new file mode 100644 index 00000000000..2e6515edd09 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 903a56ce559..14f40db2057 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -10,7 +10,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up forked-daapd from a config entry by forwarding to platform.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index 51fd312fd6d..984358f02ba 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -10,7 +10,7 @@ "websocket_not_enabled": "Forked-Daapd-Server-Websocket nicht aktiviert.", "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", - "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich." + "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version >= 27.0 erforderlich." }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index 2058bbd1cbe..51285d2f7d4 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -5,10 +5,10 @@ "not_forked_daapd": "Az eszk\u00f6z nem forked-daapd kiszolg\u00e1l\u00f3." }, "error": { - "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", + "forbidden": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "websocket_not_enabled": "forked-daapd szerver websocket nincs enged\u00e9lyezve.", - "wrong_host_or_port": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot.", + "wrong_host_or_port": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot.", "wrong_password": "Helytelen jelsz\u00f3.", "wrong_server_type": "A forked-daapd integr\u00e1ci\u00f3hoz forked-daapd szerver sz\u00fcks\u00e9ges, amelynek verzi\u00f3ja legal\u00e1bb 27.0." }, diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index 692b7ca8346..9fcc73679a7 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -10,7 +10,7 @@ "websocket_not_enabled": "forked-daapd server\u306eWebSocket\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002", "wrong_host_or_port": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "wrong_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", - "wrong_server_type": "forked-daapd \u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30d0\u30fc\u30b8\u30e7\u30f3 >= 27.0 \u306eforked-daapd\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" + "wrong_server_type": "forked-daapd \u306e\u7d71\u5408\u306b\u306f\u3001\u30d0\u30fc\u30b8\u30e7\u30f3 >= 27.0 \u306eforked-daapd\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" }, "flow_title": "{name} ({host})", "step": { @@ -34,7 +34,7 @@ "tts_pause_time": "TTS\u306e\u524d\u5f8c\u3067\u4e00\u6642\u505c\u6b62\u3059\u308b\u79d2\u6570", "tts_volume": "TTS\u30dc\u30ea\u30e5\u30fc\u30e0(\u7bc4\u56f2\u306f\u3001[0,1]\u306e\u5c0f\u6570\u70b9)" }, - "description": "forked-daapd\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u3055\u307e\u3056\u307e\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", + "description": "forked-daapd\u7d71\u5408\u306e\u3055\u307e\u3056\u307e\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", "title": "forked-daapd\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a" } } diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 2adac19c1de..ef88d0f671a 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -21,7 +21,7 @@ PLATFORMS = [Platform.CAMERA] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up foscam from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data diff --git a/homeassistant/components/foscam/translations/pt.json b/homeassistant/components/foscam/translations/pt.json index b8a454fbaba..f1afd77d051 100644 --- a/homeassistant/components/foscam/translations/pt.json +++ b/homeassistant/components/foscam/translations/pt.json @@ -1,8 +1,16 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { + "host": "Servidor", "password": "Palavra-passe" } } diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 7d7bc7695cd..12463934adb 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = router - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Services async def async_reboot(call: ServiceCall) -> None: diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index ec0085c9d32..6bd4d03bde0 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/freedompro/translations/hu.json b/homeassistant/components/freedompro/translations/hu.json index e56cc7a4a41..dd916b5b11a 100644 --- a/homeassistant/components/freedompro/translations/hu.json +++ b/homeassistant/components/freedompro/translations/hu.json @@ -12,7 +12,7 @@ "data": { "api_key": "API kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg a https://home.freedompro.eu webhelyr\u0151l kapott API-kulcsot", + "description": "K\u00e9rem, adja meg a https://home.freedompro.eu webhelyr\u0151l kapott API-kulcsot", "title": "Freedompro API kulcs" } } diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 55253fe3d1e..28036ef37e7 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await avm_wrapper.async_config_entry_first_refresh() # Load the other platforms like switch - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await async_setup_services(hass) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 4a01367cc20..d748bdcf7dd 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -240,7 +240,6 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): self.device_conn_type, "GetInfo" ).get("NewEnable") - @callback async def _async_update_data(self) -> None: """Update FritzboxTools data.""" try: diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index ddc09cb73a9..ea6461cef32 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -90,7 +90,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_check_configured_entry(self) -> ConfigEntry | None: """Check if entry is configured.""" - + assert self._host current_host = await self.hass.async_add_executor_job( socket.gethostbyname, self._host ) diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index b699cec829c..fa080a7662e 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/fritz/translations/cs.json b/homeassistant/components/fritz/translations/cs.json index 55326bb1803..8f114045caf 100644 --- a/homeassistant/components/fritz/translations/cs.json +++ b/homeassistant/components/fritz/translations/cs.json @@ -10,6 +10,7 @@ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritz/translations/pt.json b/homeassistant/components/fritz/translations/pt.json new file mode 100644 index 00000000000..b191e607a42 --- /dev/null +++ b/homeassistant/components/fritz/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 7bb71e52560..4bb2eee45b1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_migrate_entries(hass, entry.entry_id, _update_unique_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def logout_fritzbox(event: Event) -> None: """Close connections to this fritzbox.""" diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py index 812d7a7db59..a47c3c24755 100644 --- a/homeassistant/components/fritzbox_callmonitor/__init__.py +++ b/homeassistant/components/fritzbox_callmonitor/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/fritzbox_callmonitor/translations/pt.json b/homeassistant/components/fritzbox_callmonitor/translations/pt.json new file mode 100644 index 00000000000..0077ceddd46 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index f6607aed11f..c4d764f4c71 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await solar_net.init_devices() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = solar_net - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index c3b219c4b22..35881225b68 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, Final from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -109,14 +108,14 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="current_ac", - name="AC current", + name="Current AC", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_dc", - name="DC current", + name="Current DC", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -124,7 +123,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="current_dc_2", - name="DC current 2", + name="Current DC 2", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -132,14 +131,14 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="power_ac", - name="AC power", + name="Power AC", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_ac", - name="AC voltage", + name="Voltage AC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -147,7 +146,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="voltage_dc", - name="DC voltage", + name="Voltage DC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -155,7 +154,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="voltage_dc_2", - name="DC voltage 2", + name="Voltage DC 2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -492,7 +491,7 @@ OHMPILOT_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="temperature_channel_1", - name="Temperature Channel 1", + name="Temperature channel 1", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -541,7 +540,7 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="meter_mode", - name="Mode", + name="Meter mode", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( @@ -656,7 +655,8 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn """Defines a Fronius coordinator entity.""" entity_descriptions: list[SensorEntityDescription] - _entity_id_prefix: str + + _attr_has_entity_name = True def __init__( self, @@ -669,10 +669,6 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn self.entity_description = next( desc for desc in self.entity_descriptions if desc.key == key ) - # default entity_id added 2021.12 - # used for migration from non-unique_id entities of previous integration implementation - # when removed after migration period `_entity_id_prefix` will also no longer be needed - self.entity_id = f"{SENSOR_DOMAIN}.{key}_{DOMAIN}_{self._entity_id_prefix}_{coordinator.solar_net.host}" self.solar_net_id = solar_net_id self._attr_native_value = self._get_entity_value() @@ -709,7 +705,6 @@ class InverterSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius inverter sensor.""" - self._entity_id_prefix = f"inverter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) # device_info created in __init__ from a `GetInverterInfo` request self._attr_device_info = coordinator.inverter_info.device_info @@ -720,7 +715,6 @@ class LoggerSensor(_FroniusSensorEntity): """Defines a Fronius logger device sensor entity.""" entity_descriptions = LOGGER_ENTITY_DESCRIPTIONS - _entity_id_prefix = "logger_info_0" def __init__( self, @@ -749,7 +743,6 @@ class MeterSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" - self._entity_id_prefix = f"meter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) meter_data = self._device_data() # S0 meters connected directly to inverters respond "n.a." as serial number @@ -782,7 +775,6 @@ class OhmpilotSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" - self._entity_id_prefix = f"ohmpilot_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) device_data = self._device_data() @@ -801,7 +793,6 @@ class PowerFlowSensor(_FroniusSensorEntity): """Defines a Fronius power flow sensor entity.""" entity_descriptions = POWER_FLOW_ENTITY_DESCRIPTIONS - _entity_id_prefix = "power_flow_0" def __init__( self, @@ -830,7 +821,6 @@ class StorageSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius storage sensor.""" - self._entity_id_prefix = f"storage_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) storage_data = self._device_data() diff --git a/homeassistant/components/fronius/translations/pt.json b/homeassistant/components/fronius/translations/pt.json new file mode 100644 index 00000000000..f13cad90edc --- /dev/null +++ b/homeassistant/components/fronius/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 288b4796e0e..ed9b381ee9d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220707.1"], + "requirements": ["home-assistant-frontend==20220802.0"], "dependencies": [ "api", "auth", @@ -12,6 +12,7 @@ "http", "lovelace", "onboarding", + "repairs", "search", "system_log", "websocket_api" diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index b04d68e672d..1abce3b9a60 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.2.6"], + "requirements": ["afsapi==0.2.7"], "codeowners": ["@wlcrs"], "iot_class": "local_polling" } diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 01dc6b17545..5f8f3e36671 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -19,7 +19,7 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Garages Amsterdam from a config entry.""" await get_coordinator(hass) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/garages_amsterdam/translations/cs.json b/homeassistant/components/garages_amsterdam/translations/cs.json index 3b814303e69..5073c9248e0 100644 --- a/homeassistant/components/garages_amsterdam/translations/cs.json +++ b/homeassistant/components/garages_amsterdam/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } } diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py index b585658befb..56f17adc992 100644 --- a/homeassistant/components/gdacs/__init__.py +++ b/homeassistant/components/gdacs/__init__.py @@ -136,7 +136,9 @@ class GdacsFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) async def update(event_time): """Update.""" diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 5bfbd915b6c..715ac779668 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -1,12 +1,16 @@ """Geolocation support for GDACS Feed.""" from __future__ import annotations +from collections.abc import Callable import logging +from typing import Any + +from aio_georss_gdacs import GdacsFeedManager +from aio_georss_gdacs.feed_entry import GdacsFeedEntry from homeassistant.components.geo_location import GeolocationEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, @@ -17,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import GdacsFeedEntityManager from .const import DEFAULT_ICON, DOMAIN, FEED _LOGGER = logging.getLogger(__name__) @@ -52,10 +57,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the GDACS Feed platform.""" - manager = hass.data[DOMAIN][FEED][entry.entry_id] + manager: GdacsFeedEntityManager = hass.data[DOMAIN][FEED][entry.entry_id] @callback - def async_add_geolocation(feed_manager, integration_id, external_id): + def async_add_geolocation( + feed_manager: GdacsFeedManager, integration_id: str, external_id: str + ) -> None: """Add geolocation entity from feed.""" new_entity = GdacsEvent(feed_manager, integration_id, external_id) _LOGGER.debug("Adding geolocation %s", new_entity) @@ -75,16 +82,17 @@ async def async_setup_entry( class GdacsEvent(GeolocationEvent): """This represents an external event with GDACS feed data.""" - def __init__(self, feed_manager, integration_id, external_id): + _attr_should_poll = False + _attr_source = SOURCE + + def __init__( + self, feed_manager: GdacsFeedManager, integration_id: str, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager - self._integration_id = integration_id self._external_id = external_id - self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None + self._attr_unique_id = f"{integration_id}_{external_id}" + self._attr_unit_of_measurement = LENGTH_KILOMETERS self._alert_level = None self._country = None self._description = None @@ -97,11 +105,13 @@ class GdacsEvent(GeolocationEvent): self._severity = None self._vulnerability = None self._version = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._attr_unit_of_measurement = LENGTH_MILES self._remove_signal_delete = async_dispatcher_connect( self.hass, f"gdacs_delete_{self._external_id}", self._delete_callback ) @@ -119,43 +129,38 @@ class GdacsEvent(GeolocationEvent): entity_registry.async_remove(self.entity_id) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GDACS feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: GdacsFeedEntry) -> None: """Update the internal state from the provided feed entry.""" if not (event_name := feed_entry.event_name): # Earthquakes usually don't have an event name. event_name = f"{feed_entry.country} ({feed_entry.event_id})" - self._title = f"{feed_entry.event_type}: {event_name}" + self._attr_name = f"{feed_entry.event_type}: {event_name}" # Convert distance if not metric system. if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._distance = IMPERIAL_SYSTEM.length( + self._attr_distance = IMPERIAL_SYSTEM.length( feed_entry.distance_to_home, LENGTH_KILOMETERS ) else: - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._alert_level = feed_entry.alert_level self._country = feed_entry.country self._description = feed_entry.title @@ -173,57 +178,19 @@ class GdacsEvent(GeolocationEvent): self._version = feed_entry.version @property - def unique_id(self) -> str | None: - """Return a unique ID containing latitude/longitude and external id.""" - return f"{self._integration_id}_{self._external_id}" - - @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend, if any.""" if self._event_type_short and self._event_type_short in ICONS: return ICONS[self._event_type_short] return DEFAULT_ICON @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._title - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - return LENGTH_MILES - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), (ATTR_DESCRIPTION, self._description), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_EVENT_TYPE, self._event_type), (ATTR_ALERT_LEVEL, self._alert_level), (ATTR_COUNTRY, self._country), diff --git a/homeassistant/components/generic/__init__.py b/homeassistant/components/generic/__init__.py index cb669d8b906..280b468add8 100644 --- a/homeassistant/components/generic/__init__.py +++ b/homeassistant/components/generic/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up generic IP camera from a config entry.""" await _async_migrate_unique_ids(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/generic/diagnostics.py b/homeassistant/components/generic/diagnostics.py index 00be287f053..39d6a81ad88 100644 --- a/homeassistant/components/generic/diagnostics.py +++ b/homeassistant/components/generic/diagnostics.py @@ -21,12 +21,12 @@ TO_REDACT = { # A very similar redact function is in components.sql. Possible to be made common. def redact_url(data: str) -> str: """Redact credentials from string url.""" - url_in = yarl.URL(data) + url = url_in = yarl.URL(data) if url_in.user: - url = url_in.with_user("****") + url = url.with_user("****") if url_in.password: url = url.with_password("****") - if url_in.path: + if url_in.path != "/": url = url.with_path("****") if url_in.query_string: url = url.with_query("****=****") diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 5ef47f0c941..98ca2986a86 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["ha-av==10.0.0b4", "pillow==9.1.1"], + "requirements": ["ha-av==10.0.0b4", "pillow==9.2.0"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json index 818a030c8a7..90e12b8ea69 100644 --- a/homeassistant/components/generic/translations/ca.json +++ b/homeassistant/components/generic/translations/ca.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Ja hi ha una c\u00e0mera amb aquest URL de configuraci\u00f3.", "invalid_still_image": "L'URL no ha retornat una imatge fixa v\u00e0lida", + "malformed_url": "URL mal format", "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", + "relative_url": "Els URL relatius no s'admeten", "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Ja hi ha una c\u00e0mera amb aquest URL de configuraci\u00f3.", "invalid_still_image": "L'URL no ha retornat una imatge fixa v\u00e0lida", + "malformed_url": "URL mal format", "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", + "relative_url": "Els URL relatius no s'admeten", "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", diff --git a/homeassistant/components/generic/translations/cs.json b/homeassistant/components/generic/translations/cs.json index 520e0743edd..8ef4333b872 100644 --- a/homeassistant/components/generic/translations/cs.json +++ b/homeassistant/components/generic/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, "step": { "content_type": { "data": { diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json index 0c14e95a683..57d15a8efea 100644 --- a/homeassistant/components/generic/translations/de.json +++ b/homeassistant/components/generic/translations/de.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Es existiert bereits eine Kamera mit diesen URL-Einstellungen.", "invalid_still_image": "URL hat kein g\u00fcltiges Standbild zur\u00fcckgegeben", + "malformed_url": "Falsch formatierte URL", "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", + "relative_url": "Relative URLs sind nicht zul\u00e4ssig", "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Es existiert bereits eine Kamera mit diesen URL-Einstellungen.", "invalid_still_image": "URL hat kein g\u00fcltiges Standbild zur\u00fcckgegeben", + "malformed_url": "Falsch formatierte URL", "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", + "relative_url": "Relative URLs sind nicht zul\u00e4ssig", "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", diff --git a/homeassistant/components/generic/translations/el.json b/homeassistant/components/generic/translations/el.json index f97714a53c1..29063cdc216 100644 --- a/homeassistant/components/generic/translations/el.json +++ b/homeassistant/components/generic/translations/el.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 URL.", "invalid_still_image": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1", + "malformed_url": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL", "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", + "relative_url": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 URL", "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", @@ -50,7 +52,9 @@ "error": { "already_exists": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 URL.", "invalid_still_image": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1", + "malformed_url": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL", "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", + "relative_url": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 URL", "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index cb2200f9755..a4e96718225 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -13,7 +14,9 @@ "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -52,9 +55,13 @@ "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "relative_url": "Relative URLs are not allowed", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json index a50e4d4aaa7..2746f84b9a1 100644 --- a/homeassistant/components/generic/translations/et.json +++ b/homeassistant/components/generic/translations/et.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Nende URL-i seadetega kaamera on juba olemas.", "invalid_still_image": "URL ei tagastanud kehtivat pilti", + "malformed_url": "Vigane URL", "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", + "relative_url": "Osalised URL-id pole lubatud", "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Nende URL-i seadetega kaamera on juba olemas.", "invalid_still_image": "URL ei tagastanud kehtivat pilti", + "malformed_url": "Vigane URL", "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", + "relative_url": "Osalised URL-id pole lubatud", "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json index 0d517a846e7..2c992c2fa4f 100644 --- a/homeassistant/components/generic/translations/fr.json +++ b/homeassistant/components/generic/translations/fr.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Une cam\u00e9ra avec ces param\u00e8tres d'URL existe d\u00e9j\u00e0.", "invalid_still_image": "L'URL n'a pas renvoy\u00e9 d'image fixe valide", + "malformed_url": "URL mal form\u00e9e", "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", + "relative_url": "Les URL relatives ne sont pas autoris\u00e9es", "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Une cam\u00e9ra avec ces param\u00e8tres d'URL existe d\u00e9j\u00e0.", "invalid_still_image": "L'URL n'a pas renvoy\u00e9 d'image fixe valide", + "malformed_url": "URL mal form\u00e9e", "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", + "relative_url": "Les URL relatives ne sont pas autoris\u00e9es", "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", diff --git a/homeassistant/components/generic/translations/hu.json b/homeassistant/components/generic/translations/hu.json index 76992a1fde7..bf03ec88d96 100644 --- a/homeassistant/components/generic/translations/hu.json +++ b/homeassistant/components/generic/translations/hu.json @@ -7,7 +7,9 @@ "error": { "already_exists": "M\u00e1r l\u00e9tezik egy kamera ezekkel az URL-be\u00e1ll\u00edt\u00e1sokkal.", "invalid_still_image": "Az URL nem adott vissza \u00e9rv\u00e9nyes \u00e1ll\u00f3k\u00e9pet", + "malformed_url": "Hib\u00e1s URL", "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", + "relative_url": "A relat\u00edv URL-ek nem enged\u00e9lyezettek", "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", @@ -50,7 +52,9 @@ "error": { "already_exists": "M\u00e1r l\u00e9tezik egy kamera ezekkel az URL-be\u00e1ll\u00edt\u00e1sokkal.", "invalid_still_image": "Az URL nem adott vissza \u00e9rv\u00e9nyes \u00e1ll\u00f3k\u00e9pet", + "malformed_url": "Hib\u00e1s URL", "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", + "relative_url": "A relat\u00edv URL-ek nem enged\u00e9lyezettek", "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index a9c553580ca..5222c111c58 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Kamera dengan setelan URL ini sudah ada.", "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", + "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", + "relative_url": "URL relatif tidak diizinkan", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Kamera dengan setelan URL ini sudah ada.", "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", + "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", + "relative_url": "URL relatif tidak diizinkan", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 1cd63544700..14d4b6e8720 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", + "malformed_url": "URL non valido", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", + "relative_url": "Non sono consentiti URL relativi", "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", + "malformed_url": "URL non valido", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", + "relative_url": "Non sono consentiti URL relativi", "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index f4fd7d8ec46..8e1ba58b4df 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -2,12 +2,14 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", "invalid_still_image": "URL\u304c\u6709\u52b9\u306a\u9759\u6b62\u753b\u50cf\u3092\u8fd4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", + "malformed_url": "\u4e0d\u6b63\u306a\u5f62\u5f0f\u306eURL", "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "relative_url": "\u76f8\u5bfeURL(Relative URLs)\u306f\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", @@ -50,7 +52,9 @@ "error": { "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", "invalid_still_image": "URL\u304c\u6709\u52b9\u306a\u9759\u6b62\u753b\u50cf\u3092\u8fd4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", + "malformed_url": "\u4e0d\u6b63\u306a\u5f62\u5f0f\u306eURL", "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "relative_url": "\u76f8\u5bfeURL(Relative URLs)\u306f\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index 72355d002ed..5c718dc0f47 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Et kamera med disse URL-innstillingene finnes allerede.", "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", + "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", + "relative_url": "Relative URL-adresser ikke tillatt", "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", "stream_io_error": "Inn-/utdatafeil under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", @@ -50,6 +52,7 @@ "error": { "already_exists": "Et kamera med disse URL-innstillingene finnes allerede.", "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", + "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json index 81817faf236..71c50148957 100644 --- a/homeassistant/components/generic/translations/pl.json +++ b/homeassistant/components/generic/translations/pl.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Kamera z tymi ustawieniami adresu URL ju\u017c istnieje.", "invalid_still_image": "Adres URL nie zwr\u00f3ci\u0142 prawid\u0142owego obrazu nieruchomego (still image)", + "malformed_url": "Nieprawid\u0142owy adres URL", "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", + "relative_url": "Wzgl\u0119dne adresy URL s\u0105 niedozwolone", "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Kamera z tymi ustawieniami adresu URL ju\u017c istnieje.", "invalid_still_image": "Adres URL nie zwr\u00f3ci\u0142 prawid\u0142owego obrazu nieruchomego (still image)", + "malformed_url": "Nieprawid\u0142owy adres URL", "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", + "relative_url": "Wzgl\u0119dne adresy URL s\u0105 niedozwolone", "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json index 1a61cdeac97..86ac7a01efb 100644 --- a/homeassistant/components/generic/translations/pt-BR.json +++ b/homeassistant/components/generic/translations/pt-BR.json @@ -7,7 +7,9 @@ "error": { "already_exists": "J\u00e1 existe uma c\u00e2mera com essas configura\u00e7\u00f5es de URL.", "invalid_still_image": "A URL n\u00e3o retornou uma imagem est\u00e1tica v\u00e1lida", + "malformed_url": "URL malformada", "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", + "relative_url": "URLs relativas n\u00e3o s\u00e3o permitidas", "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", @@ -50,7 +52,9 @@ "error": { "already_exists": "J\u00e1 existe uma c\u00e2mera com essas configura\u00e7\u00f5es de URL.", "invalid_still_image": "A URL n\u00e3o retornou uma imagem est\u00e1tica v\u00e1lida", + "malformed_url": "URL malformada", "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", + "relative_url": "URLs relativas n\u00e3o s\u00e3o permitidas", "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", diff --git a/homeassistant/components/generic/translations/pt.json b/homeassistant/components/generic/translations/pt.json new file mode 100644 index 00000000000..06abd0a7dfa --- /dev/null +++ b/homeassistant/components/generic/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "authentication": "Autentica\u00e7\u00e3o", + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json index f9fed6b5831..022af07b58b 100644 --- a/homeassistant/components/generic/translations/ru.json +++ b/homeassistant/components/generic/translations/ru.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u041a\u0430\u043c\u0435\u0440\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c URL-\u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", "invalid_still_image": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435.", + "malformed_url": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", + "relative_url": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f.", "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", @@ -15,6 +17,7 @@ "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "template_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0448\u0430\u0431\u043b\u043e\u043d\u0430. \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." @@ -49,7 +52,9 @@ "error": { "already_exists": "\u041a\u0430\u043c\u0435\u0440\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c URL-\u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", "invalid_still_image": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435.", + "malformed_url": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", + "relative_url": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f.", "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", @@ -57,6 +62,7 @@ "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "template_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0448\u0430\u0431\u043b\u043e\u043d\u0430. \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json index d439c559aa5..efce9d014b1 100644 --- a/homeassistant/components/generic/translations/tr.json +++ b/homeassistant/components/generic/translations/tr.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Bu URL ayarlar\u0131na sahip bir kamera zaten var.", "invalid_still_image": "URL ge\u00e7erli bir hareketsiz resim d\u00f6nd\u00fcrmedi", + "malformed_url": "Hatal\u0131 bi\u00e7imlendirilmi\u015f URL", "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", + "relative_url": "G\u00f6receli URL'lere izin verilmez", "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Bu URL ayarlar\u0131na sahip bir kamera zaten var.", "invalid_still_image": "URL ge\u00e7erli bir hareketsiz resim d\u00f6nd\u00fcrmedi", + "malformed_url": "Hatal\u0131 bi\u00e7imlendirilmi\u015f URL", "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", + "relative_url": "G\u00f6receli URL'lere izin verilmez", "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json index 595bf019f64..ded2ea569c4 100644 --- a/homeassistant/components/generic/translations/zh-Hant.json +++ b/homeassistant/components/generic/translations/zh-Hant.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u5df2\u7d93\u6709\u4f7f\u7528\u76f8\u540c URL \u7684\u651d\u5f71\u6a5f\u3002", "invalid_still_image": "URL \u56de\u50b3\u4e26\u975e\u6709\u6548\u975c\u614b\u5f71\u50cf", + "malformed_url": "URL \u683c\u5f0f\u932f\u8aa4", "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", + "relative_url": "\u4e0d\u5141\u8a31\u4f7f\u7528\u76f8\u5c0d\u61c9 URL", "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", @@ -50,7 +52,9 @@ "error": { "already_exists": "\u5df2\u7d93\u6709\u4f7f\u7528\u76f8\u540c URL \u7684\u651d\u5f71\u6a5f\u3002", "invalid_still_image": "URL \u56de\u50b3\u4e26\u975e\u6709\u6548\u975c\u614b\u5f71\u50cf", + "malformed_url": "URL \u683c\u5f0f\u932f\u8aa4", "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", + "relative_url": "\u4e0d\u5141\u8a31\u4f7f\u7528\u76f8\u5c0d\u61c9 URL", "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index fa8b05b5ef8..8072c76ea07 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -173,7 +173,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): if self._keep_alive: async_track_time_interval(self.hass, self._async_operate, self._keep_alive) - @callback async def _async_startup(event): """Init on startup.""" sensor_state = self.hass.states.get(self._sensor_entity_id) @@ -309,7 +308,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): # Get default humidity from super class return super().max_humidity - @callback async def _async_sensor_changed(self, entity_id, old_state, new_state): """Handle ambient humidity changes.""" if new_state is None: @@ -328,7 +326,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): await self._async_operate() await self.async_update_ha_state() - @callback async def _async_sensor_not_responding(self, now=None): """Handle sensor stale event.""" diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index 896fb7d36da..c0d6abe694f 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -1,10 +1,13 @@ """Support for generic GeoJSON events.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import logging +from typing import Any from aio_geojson_generic_client import GenericFeedManager +from aio_geojson_generic_client.feed_entry import GenericFeedEntry import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -17,7 +20,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -55,20 +58,20 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the GeoJSON Events platform.""" - url = config[CONF_URL] - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + url: str = config[CONF_URL] + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] + radius_in_km: float = config[CONF_RADIUS] # Initialize the entity manager. manager = GeoJsonFeedEntityManager( hass, async_add_entities, scan_interval, coordinates, url, radius_in_km ) await manager.async_init() - async def start_feed_manager(event=None): + async def start_feed_manager(event: Event) -> None: """Start feed manager.""" await manager.async_update() @@ -79,8 +82,14 @@ class GeoJsonFeedEntityManager: """Feed Entity Manager for GeoJSON feeds.""" def __init__( - self, hass, async_add_entities, scan_interval, coordinates, url, radius_in_km - ): + self, + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + url: str, + radius_in_km: float, + ) -> None: """Initialize the GeoJSON Feed Manager.""" self._hass = hass @@ -97,10 +106,10 @@ class GeoJsonFeedEntityManager: self._async_add_entities = async_add_entities self._scan_interval = scan_interval - async def async_init(self): + async def async_init(self) -> None: """Schedule initial and regular updates based on configured time interval.""" - async def update(event_time): + async def update(event_time: datetime) -> None: """Update.""" await self.async_update() @@ -108,26 +117,26 @@ class GeoJsonFeedEntityManager: async_track_time_interval(self._hass, update, self._scan_interval) _LOGGER.debug("Feed entity manager initialized") - async def async_update(self): + async def async_update(self) -> None: """Refresh data.""" await self._feed_manager.update() _LOGGER.debug("Feed entity manager updated") - def get_entry(self, external_id): + def get_entry(self, external_id: str) -> GenericFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - async def _generate_entity(self, external_id): + async def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = GeoJsonLocationEvent(self, external_id) # Add new entities to HA. self._async_add_entities([new_entity], True) - async def _update_entity(self, external_id): + async def _update_entity(self, external_id: str) -> None: """Update entity.""" async_dispatcher_send(self._hass, f"geo_json_events_update_{external_id}") - async def _remove_entity(self, external_id): + async def _remove_entity(self, external_id: str) -> None: """Remove entity.""" async_dispatcher_send(self._hass, f"geo_json_events_delete_{external_id}") @@ -135,18 +144,18 @@ class GeoJsonFeedEntityManager: class GeoJsonLocationEvent(GeolocationEvent): """This represents an external event with GeoJSON data.""" - def __init__(self, feed_manager, external_id): + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__(self, feed_manager: GenericFeedManager, external_id: str) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -160,68 +169,33 @@ class GeoJsonLocationEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GeoJSON location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: GenericFeedEntry) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" if not self._external_id: return {} diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index e58391ca84a..54542fa8503 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import final +from typing import Any, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE @@ -54,8 +54,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class GeolocationEvent(Entity): """Base class for an external event with an associated geolocation.""" + # Entity Properties + _attr_source: str + _attr_distance: float | None = None + _attr_latitude: float | None = None + _attr_longitude: float | None = None + + @final @property - def state(self): + def state(self) -> float | None: """Return the state of the sensor.""" if self.distance is not None: return round(self.distance, 1) @@ -64,32 +71,30 @@ class GeolocationEvent(Entity): @property def source(self) -> str: """Return source value of this external event.""" - raise NotImplementedError + return self._attr_source @property def distance(self) -> float | None: """Return distance value of this external event.""" - return None + return self._attr_distance @property def latitude(self) -> float | None: """Return latitude value of this external event.""" - return None + return self._attr_latitude @property def longitude(self) -> float | None: """Return longitude value of this external event.""" - return None + return self._attr_longitude @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return the state attributes of this external event.""" - data = {} + data: dict[str, Any] = {ATTR_SOURCE: self.source} if self.latitude is not None: data[ATTR_LATITUDE] = round(self.latitude, 5) if self.longitude is not None: data[ATTR_LONGITUDE] = round(self.longitude, 5) - if self.source is not None: - data[ATTR_SOURCE] = self.source return data diff --git a/homeassistant/components/geocaching/__init__.py b/homeassistant/components/geocaching/__init__.py index 430cbc9a8d0..aa2926df949 100644 --- a/homeassistant/components/geocaching/__init__.py +++ b/homeassistant/components/geocaching/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/geocaching/sensor.py b/homeassistant/components/geocaching/sensor.py index 0c719c463a4..134877d7509 100644 --- a/homeassistant/components/geocaching/sensor.py +++ b/homeassistant/components/geocaching/sensor.py @@ -91,6 +91,7 @@ class GeocachingSensor( """Representation of a Sensor.""" entity_description: GeocachingSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -100,9 +101,6 @@ class GeocachingSensor( """Initialize the Geocaching sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = ( - f"Geocaching {coordinator.data.user.username} {description.name}" - ) self._attr_unique_id = ( f"{coordinator.data.user.reference_code}_{description.key}" ) diff --git a/homeassistant/components/geocaching/translations/ja.json b/homeassistant/components/geocaching/translations/ja.json index 41a238aa637..5de3aba8a04 100644 --- a/homeassistant/components/geocaching/translations/ja.json +++ b/homeassistant/components/geocaching/translations/ja.json @@ -17,8 +17,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Geocaching\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Geocaching\u7d71\u5408\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/geocaching/translations/pt.json b/homeassistant/components/geocaching/translations/pt.json new file mode 100644 index 00000000000..8685cf2d3c2 --- /dev/null +++ b/homeassistant/components/geocaching/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index d3b0fcbe81a..2e6ed8429dd 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -144,7 +144,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/geofency/translations/ja.json b/homeassistant/components/geofency/translations/ja.json index e653cb99d43..812ba39b940 100644 --- a/homeassistant/components/geofency/translations/ja.json +++ b/homeassistant/components/geofency/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index d9c27e9dff8..6c091e71f05 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -144,7 +144,9 @@ class GeonetnzQuakesFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) async def update(event_time): """Update.""" diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 515fce56439..26ad780d098 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -1,12 +1,15 @@ """Geolocation support for GeoNet NZ Quakes Feeds.""" from __future__ import annotations +from collections.abc import Callable import logging +from typing import Any + +from aio_geojson_geonetnz_quakes.feed_entry import GeonetnzQuakesFeedEntry from homeassistant.components.geo_location import GeolocationEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_TIME, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, @@ -18,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import GeonetnzQuakesFeedEntityManager from .const import DOMAIN, FEED _LOGGER = logging.getLogger(__name__) @@ -40,10 +44,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the GeoNet NZ Quakes Feed platform.""" - manager = hass.data[DOMAIN][FEED][entry.entry_id] + manager: GeonetnzQuakesFeedEntityManager = hass.data[DOMAIN][FEED][entry.entry_id] @callback - def async_add_geolocation(feed_manager, integration_id, external_id): + def async_add_geolocation( + feed_manager: GeonetnzQuakesFeedEntityManager, + integration_id: str, + external_id: str, + ) -> None: """Add geolocation entity from feed.""" new_entity = GeonetnzQuakesEvent(feed_manager, integration_id, external_id) _LOGGER.debug("Adding geolocation %s", new_entity) @@ -63,27 +71,34 @@ async def async_setup_entry( class GeonetnzQuakesEvent(GeolocationEvent): """This represents an external event with GeoNet NZ Quakes feed data.""" - def __init__(self, feed_manager, integration_id, external_id): + _attr_icon = "mdi:pulse" + _attr_should_poll = False + _attr_source = SOURCE + + def __init__( + self, + feed_manager: GeonetnzQuakesFeedEntityManager, + integration_id: str, + external_id: str, + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager - self._integration_id = integration_id self._external_id = external_id - self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None + self._attr_unique_id = f"{integration_id}_{external_id}" + self._attr_unit_of_measurement = LENGTH_KILOMETERS self._depth = None self._locality = None self._magnitude = None self._mmi = None self._quality = None self._time = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._attr_unit_of_measurement = LENGTH_MILES self._remove_signal_delete = async_dispatcher_connect( self.hass, f"geonetnz_quakes_delete_{self._external_id}", @@ -105,40 +120,35 @@ class GeonetnzQuakesEvent(GeolocationEvent): entity_registry.async_remove(self.entity_id) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GeoNet NZ Quakes feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: GeonetnzQuakesFeedEntry) -> None: """Update the internal state from the provided feed entry.""" - self._title = feed_entry.title + self._attr_name = feed_entry.title # Convert distance if not metric system. if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._distance = IMPERIAL_SYSTEM.length( + self._attr_distance = IMPERIAL_SYSTEM.length( feed_entry.distance_to_home, LENGTH_KILOMETERS ) else: - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._depth = feed_entry.depth self._locality = feed_entry.locality self._magnitude = feed_entry.magnitude @@ -147,54 +157,11 @@ class GeonetnzQuakesEvent(GeolocationEvent): self._time = feed_entry.time @property - def unique_id(self) -> str | None: - """Return a unique ID containing latitude/longitude and external id.""" - return f"{self._integration_id}_{self._external_id}" - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._title - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - return LENGTH_MILES - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_DEPTH, self._depth), (ATTR_LOCALITY, self._locality), (ATTR_MAGNITUDE, self._magnitude), diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index 8d0733aacd1..a1b6368c8ef 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -130,7 +130,9 @@ class GeonetnzVolcanoFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) async def update(event_time): """Update.""" diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 73d773561f5..adef70e5864 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove air_quality entities from registry if they exist ent_reg = er.async_get(hass) diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 858a756e3e3..895775495f9 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -4,15 +4,9 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - -from .model import GiosSensorEntityDescription - ATTRIBUTION: Final = "Data provided by GIOŚ" CONF_STATION_ID: Final = "station_id" -DEFAULT_NAME: Final = "GIOŚ" # Term of service GIOŚ allow downloading data no more than twice an hour. SCAN_INTERVAL: Final = timedelta(minutes=30) DOMAIN: Final = "gios" @@ -33,61 +27,3 @@ ATTR_PM10: Final = "pm10" ATTR_PM25: Final = "pm25" ATTR_SO2: Final = "so2" ATTR_AQI: Final = "aqi" - -SENSOR_TYPES: Final[tuple[GiosSensorEntityDescription, ...]] = ( - GiosSensorEntityDescription( - key=ATTR_AQI, - name="AQI", - device_class=SensorDeviceClass.AQI, - value=None, - ), - GiosSensorEntityDescription( - key=ATTR_C6H6, - name="C6H6", - icon="mdi:molecule", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_CO, - name="CO", - device_class=SensorDeviceClass.CO, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_NO2, - name="NO2", - device_class=SensorDeviceClass.NITROGEN_DIOXIDE, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_O3, - name="O3", - device_class=SensorDeviceClass.OZONE, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_PM10, - name="PM10", - device_class=SensorDeviceClass.PM10, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_PM25, - name="PM2.5", - device_class=SensorDeviceClass.PM25, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_SO2, - name="SO2", - device_class=SensorDeviceClass.SULPHUR_DIOXIDE, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), -) diff --git a/homeassistant/components/gios/model.py b/homeassistant/components/gios/model.py deleted file mode 100644 index 0f5d992590b..00000000000 --- a/homeassistant/components/gios/model.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Type definitions for GIOS integration.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class GiosSensorEntityDescription(SensorEntityDescription): - """Class describing GIOS sensor entities.""" - - value: Callable | None = round diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 391976ad793..2d32b8261f3 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -1,12 +1,25 @@ """Support for the GIOS service.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import logging from typing import Any, cast -from homeassistant.components.sensor import DOMAIN as PLATFORM, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN as PLATFORM, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_NAME, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_NAME, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONF_NAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType @@ -18,21 +31,90 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import GiosDataUpdateCoordinator from .const import ( ATTR_AQI, + ATTR_C6H6, + ATTR_CO, ATTR_INDEX, + ATTR_NO2, + ATTR_O3, + ATTR_PM10, ATTR_PM25, + ATTR_SO2, ATTR_STATION, ATTRIBUTION, - DEFAULT_NAME, DOMAIN, MANUFACTURER, - SENSOR_TYPES, URL, ) -from .model import GiosSensorEntityDescription _LOGGER = logging.getLogger(__name__) +@dataclass +class GiosSensorEntityDescription(SensorEntityDescription): + """Class describing GIOS sensor entities.""" + + value: Callable | None = round + + +SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( + GiosSensorEntityDescription( + key=ATTR_AQI, + name="AQI", + device_class=SensorDeviceClass.AQI, + value=None, + ), + GiosSensorEntityDescription( + key=ATTR_C6H6, + name="C6H6", + icon="mdi:molecule", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_CO, + name="CO", + device_class=SensorDeviceClass.CO, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_NO2, + name="NO2", + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_O3, + name="O3", + device_class=SensorDeviceClass.OZONE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_PM10, + name="PM10", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_PM25, + name="PM2.5", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_SO2, + name="SO2", + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -72,6 +154,7 @@ async def async_setup_entry( class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity): """Define an GIOS sensor.""" + _attr_has_entity_name = True entity_description: GiosSensorEntityDescription def __init__( @@ -86,10 +169,9 @@ class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, str(coordinator.gios.station_id))}, manufacturer=MANUFACTURER, - name=DEFAULT_NAME, + name=name, configuration_url=URL.format(station_id=coordinator.gios.station_id), ) - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" self._attrs: dict[str, Any] = { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/gios/translations/pl.json b/homeassistant/components/gios/translations/pl.json index 6a7d2aa7064..475ab76b576 100644 --- a/homeassistant/components/gios/translations/pl.json +++ b/homeassistant/components/gios/translations/pl.json @@ -20,7 +20,7 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera GIO\u015a" + "can_reach_server": "Dost\u0119p do serwera" } } } \ No newline at end of file diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 404aeae11b5..53b8cd67871 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_cleanup_device_registry(hass=hass, entry=entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 8dff4b04b01..d17f762fedd 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -89,7 +89,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="pulls_count", - name="Pull Requests", + name="Pull requests", native_unit_of_measurement="Pull Requests", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -97,7 +97,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_commit", - name="Latest Commit", + name="Latest commit", value_fn=lambda data: data["default_branch_ref"]["commit"]["message"][:255], attr_fn=lambda data: { "sha": data["default_branch_ref"]["commit"]["sha"], @@ -106,7 +106,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_discussion", - name="Latest Discussion", + name="Latest discussion", avabl_fn=lambda data: data["discussion"]["discussions"], value_fn=lambda data: data["discussion"]["discussions"][0]["title"][:255], attr_fn=lambda data: { @@ -116,7 +116,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_release", - name="Latest Release", + name="Latest release", avabl_fn=lambda data: data["release"] is not None, value_fn=lambda data: data["release"]["name"][:255], attr_fn=lambda data: { @@ -126,7 +126,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_issue", - name="Latest Issue", + name="Latest issue", avabl_fn=lambda data: data["issue"]["issues"], value_fn=lambda data: data["issue"]["issues"][0]["title"][:255], attr_fn=lambda data: { @@ -136,7 +136,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_pull_request", - name="Latest Pull Request", + name="Latest pull request", avabl_fn=lambda data: data["pull_request"]["pull_requests"], value_fn=lambda data: data["pull_request"]["pull_requests"][0]["title"][:255], attr_fn=lambda data: { @@ -146,7 +146,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_tag", - name="Latest Tag", + name="Latest tag", avabl_fn=lambda data: data["refs"]["tags"], value_fn=lambda data: data["refs"]["tags"][0]["name"][:255], attr_fn=lambda data: { @@ -176,6 +176,7 @@ class GitHubSensorEntity(CoordinatorEntity[GitHubDataUpdateCoordinator], SensorE """Defines a GitHub sensor entity.""" _attr_attribution = "Data provided by the GitHub API" + _attr_has_entity_name = True entity_description: GitHubSensorEntityDescription @@ -188,9 +189,6 @@ class GitHubSensorEntity(CoordinatorEntity[GitHubDataUpdateCoordinator], SensorE super().__init__(coordinator=coordinator) self.entity_description = entity_description - self._attr_name = ( - f"{coordinator.data.get('full_name')} {entity_description.name}" - ) self._attr_unique_id = f"{coordinator.data.get('id')}_{entity_description.key}" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/github/translations/ja.json b/homeassistant/components/github/translations/ja.json index 6b5c62e4afd..f7dfc37f64d 100644 --- a/homeassistant/components/github/translations/ja.json +++ b/homeassistant/components/github/translations/ja.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "could_not_register": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3068GitHub\u3068\u306e\u767b\u9332\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + "could_not_register": "\u7d71\u5408\u3068GitHub\u3068\u306e\u767b\u9332\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "progress": { - "wait_for_device": "1. {url} \u3092\u958b\u304f\n2. \u6b21\u306e\u30ad\u30fc\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\n```\n{code}\n```\n" + "wait_for_device": "1. {url} \u3092\u958b\u304f\n2. \u6b21\u306e\u30ad\u30fc\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u3001\u7d71\u5408\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\n```\n{code}\n```\n" }, "step": { "repositories": { diff --git a/homeassistant/components/github/translations/pt.json b/homeassistant/components/github/translations/pt.json new file mode 100644 index 00000000000..d252c078a2c --- /dev/null +++ b/homeassistant/components/github/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 571214deb20..2b52cdeef8b 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -91,7 +91,9 @@ class GlancesData: self.config_entry.add_update_listener(self.async_options_updated) ) - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) return True diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 7dfd0c503ef..e37cfaca211 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,8 +1,9 @@ """Support gathering system information of hosts which are running glances.""" from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE +from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -18,14 +19,34 @@ async def async_setup_entry( ) -> None: """Set up the Glances sensors.""" - client = hass.data[DOMAIN][config_entry.entry_id] + client: GlancesData = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] + @callback + def _migrate_old_unique_ids( + hass: HomeAssistant, old_unique_id: str, new_key: str + ) -> None: + """Migrate unique IDs to the new format.""" + ent_reg = entity_registry.async_get(hass) + + if entity_id := ent_reg.async_get_entity_id( + Platform.SENSOR, DOMAIN, old_unique_id + ): + + ent_reg.async_update_entity( + entity_id, new_unique_id=f"{config_entry.entry_id}-{new_key}" + ) + for description in SENSOR_TYPES: if description.type == "fs": # fs will provide a list of disks attached for disk in client.api.data[description.type]: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {disk['mnt_point']} {description.name_suffix}", + f"{disk['mnt_point']}-{description.key}", + ) dev.append( GlancesSensor( client, @@ -38,6 +59,11 @@ async def async_setup_entry( # sensors will provide temp for different devices for sensor in client.api.data[description.type]: if sensor["type"] == description.key: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {sensor['label']} {description.name_suffix}", + f"{sensor['label']}-{description.key}", + ) dev.append( GlancesSensor( client, @@ -48,8 +74,18 @@ async def async_setup_entry( ) elif description.type == "raid": for raid_device in client.api.data[description.type]: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {raid_device} {description.name_suffix}", + f"{raid_device}-{description.key}", + ) dev.append(GlancesSensor(client, name, raid_device, description)) elif client.api.data[description.type]: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {description.name_suffix}", + f"-{description.key}", + ) dev.append( GlancesSensor( client, @@ -87,11 +123,7 @@ class GlancesSensor(SensorEntity): manufacturer="Glances", name=name, ) - - @property - def unique_id(self): - """Set unique_id for sensor.""" - return f"{self.glances_data.host}-{self.name}" + self._attr_unique_id = f"{self.glances_data.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" @property def available(self): diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index ea292a651c2..f32cad5a488 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = GoalZeroDataUpdateCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index c4219b51e6c..db8a1b788f2 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -26,7 +26,7 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( ), BinarySensorEntityDescription( key="app_online", - name="App Online", + name="App online", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -37,7 +37,7 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( ), BinarySensorEntityDescription( key="inputDetected", - name="Input Detected", + name="Input detected", device_class=BinarySensorDeviceClass.POWER, ), ) diff --git a/homeassistant/components/goalzero/entity.py b/homeassistant/components/goalzero/entity.py index 8c696ce1377..eef6ea43d9c 100644 --- a/homeassistant/components/goalzero/entity.py +++ b/homeassistant/components/goalzero/entity.py @@ -15,6 +15,7 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): """Representation of a Goal Zero Yeti entity.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -24,9 +25,6 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): """Initialize a Goal Zero Yeti entity.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = ( - f"{coordinator.config_entry.data[CONF_NAME]} {description.name}" - ) self._attr_unique_id = f"{coordinator.config_entry.entry_id}/{description.key}" @property @@ -37,7 +35,7 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, manufacturer=MANUFACTURER, model=self._api.sysdata[ATTR_MODEL], - name=self.coordinator.config_entry.data[CONF_NAME], + name=self.coordinator.config_entry.data[CONF_NAME].capitalize(), sw_version=self._api.data["firmwareVersion"], ) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index ef95578820d..345c3b41f7d 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -32,14 +32,14 @@ from .entity import GoalZeroEntity SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wattsIn", - name="Watts In", + name="Watts in", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ampsIn", - name="Amps In", + name="Amps in", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -47,14 +47,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wattsOut", - name="Watts Out", + name="Watts out", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ampsOut", - name="Amps Out", + name="Amps out", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -62,7 +62,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="whOut", - name="WH Out", + name="Wh out", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, @@ -70,7 +70,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="whStored", - name="WH Stored", + name="Wh stored", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=SensorStateClass.MEASUREMENT, @@ -84,13 +84,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="socPercent", - name="State of Charge Percent", + name="State of charge percent", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="timeToEmptyFull", - name="Time to Empty/Full", + name="Time to empty/full", device_class=TIME_MINUTES, native_unit_of_measurement=TIME_MINUTES, ), @@ -103,7 +103,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wifiStrength", - name="Wifi Strength", + name="Wi-Fi strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, entity_registry_enabled_default=False, @@ -111,7 +111,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="timestamp", - name="Total Run Time", + name="Total run time", native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -124,7 +124,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="ipAddr", - name="IP Address", + name="IP address", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 9a58cb385b6..25fdeb52114 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -14,15 +14,15 @@ from .entity import GoalZeroEntity SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="v12PortStatus", - name="12V Port Status", + name="12V port status", ), SwitchEntityDescription( key="usbPortStatus", - name="USB Port Status", + name="USB port status", ), SwitchEntityDescription( key="acPortStatus", - name="AC Port Status", + name="AC port status", ), ) diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json index c0376fc29b9..607cc4e5b63 100644 --- a/homeassistant/components/goalzero/translations/hu.json +++ b/homeassistant/components/goalzero/translations/hu.json @@ -19,7 +19,7 @@ "host": "C\u00edm", "name": "Elnevez\u00e9s" }, - "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." + "description": "K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." } } } diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index 7dccd5551c7..ece2f6bbbc8 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data_update_coordinator = get_data_update_coordinator(hass, entry) await data_update_coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/goodwe/__init__.py b/homeassistant/components/goodwe/__init__.py index e48590931ba..5dcc66e90d4 100644 --- a/homeassistant/components/goodwe/__init__.py +++ b/homeassistant/components/goodwe/__init__.py @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/goodwe/manifest.json b/homeassistant/components/goodwe/manifest.json index 45895d0d4b0..c91b91c02a9 100644 --- a/homeassistant/components/goodwe/manifest.json +++ b/homeassistant/components/goodwe/manifest.json @@ -3,7 +3,7 @@ "name": "GoodWe Inverter", "documentation": "https://www.home-assistant.io/integrations/goodwe", "codeowners": ["@mletenay", "@starkillerOG"], - "requirements": ["goodwe==0.2.15"], + "requirements": ["goodwe==0.2.18"], "config_flow": true, "iot_class": "local_polling", "loggers": ["goodwe"] diff --git a/homeassistant/components/goodwe/translations/pt.json b/homeassistant/components/goodwe/translations/pt.json new file mode 100644 index 00000000000..410be6f924f --- /dev/null +++ b/homeassistant/components/goodwe/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 5553350aa23..4b72aaa77ad 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -20,6 +20,7 @@ from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, @@ -30,7 +31,11 @@ from homeassistant.const import ( CONF_OFFSET, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -199,21 +204,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: }, ) ) - - _LOGGER.warning( - "Configuration of Google Calendar in YAML in configuration.yaml is " - "is deprecated and will be removed in a future release; Your existing " - "OAuth Application Credentials and access settings have been imported " - "into the UI automatically and can be safely removed from your " - "configuration.yaml file" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", # Warning first added in 2022.6.0 + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", ) if conf.get(CONF_TRACK_NEW) is False: # The track_new as False would previously result in new entries - # in google_calendars.yaml with track set to Fasle which is + # in google_calendars.yaml with track set to False which is # handled at calendar entity creation time. - _LOGGER.warning( - "You must manually set the integration System Options in the " - "UI to disable newly discovered entities going forward" + async_create_issue( + hass, + DOMAIN, + "removed_track_new_yaml", + breaks_in_ha_version="2022.6.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_track_new_yaml", ) return True @@ -268,7 +279,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if get_feature_access(hass, entry) is FeatureAccess.read_write: await async_setup_add_event_service(hass, calendar_service) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) @@ -348,15 +359,18 @@ async def async_setup_add_event_service( "Missing required fields to set start or end date/datetime" ) - await calendar_service.async_create_event( - call.data[EVENT_CALENDAR_ID], - Event( - summary=call.data[EVENT_SUMMARY], - description=call.data[EVENT_DESCRIPTION], - start=start, - end=end, - ), - ) + try: + await calendar_service.async_create_event( + call.data[EVENT_CALENDAR_ID], + Event( + summary=call.data[EVENT_SUMMARY], + description=call.data[EVENT_DESCRIPTION], + start=start, + end=end, + ), + ) + except ApiException as err: + raise HomeAssistantError(str(err)) from err hass.services.async_register( DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 3c271a2c3c3..ca98b3da087 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -2,7 +2,6 @@ from __future__ import annotations -import copy from datetime import datetime, timedelta import logging from typing import Any @@ -21,8 +20,8 @@ from homeassistant.components.calendar import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import PlatformNotReady +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import ( config_validation as cv, entity_platform, @@ -30,7 +29,11 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from . import ( CONF_IGNORE_AVAILABILITY, @@ -182,9 +185,17 @@ async def async_setup_entry( entity_registry.async_remove( entity_entry.entity_id, ) + coordinator = CalendarUpdateCoordinator( + hass, + calendar_service, + data[CONF_NAME], + calendar_id, + data.get(CONF_SEARCH), + ) + await coordinator.async_config_entry_first_refresh() entities.append( GoogleCalendarEntity( - calendar_service, + coordinator, calendar_id, data, generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass), @@ -213,12 +224,66 @@ async def async_setup_entry( ) -class GoogleCalendarEntity(CalendarEntity): - """A calendar event device.""" +class CalendarUpdateCoordinator(DataUpdateCoordinator): + """Coordinator for calendar RPC calls.""" def __init__( self, + hass: HomeAssistant, calendar_service: GoogleCalendarService, + name: str, + calendar_id: str, + search: str | None, + ) -> None: + """Create the Calendar event device.""" + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + self.calendar_service = calendar_service + self.calendar_id = calendar_id + self._search = search + + async def async_get_events( + self, start_date: datetime, end_date: datetime + ) -> list[Event]: + """Get all events in a specific time frame.""" + request = ListEventsRequest( + calendar_id=self.calendar_id, + start_time=start_date, + end_time=end_date, + search=self._search, + ) + result_items = [] + try: + result = await self.calendar_service.async_list_events(request) + async for result_page in result: + result_items.extend(result_page.items) + except ApiException as err: + self.async_set_update_error(err) + raise HomeAssistantError(str(err)) from err + return result_items + + async def _async_update_data(self) -> list[Event]: + """Fetch data from API endpoint.""" + request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) + try: + result = await self.calendar_service.async_list_events(request) + except ApiException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + return result.items + + +class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): + """A calendar event entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: CalendarUpdateCoordinator, calendar_id: str, data: dict[str, Any], entity_id: str, @@ -226,18 +291,29 @@ class GoogleCalendarEntity(CalendarEntity): entity_enabled: bool, ) -> None: """Create the Calendar event device.""" - self.calendar_service = calendar_service + super().__init__(coordinator) + self.coordinator = coordinator self.calendar_id = calendar_id - self._search: str | None = data.get(CONF_SEARCH) self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) self._event: CalendarEvent | None = None - self._name: str = data[CONF_NAME] + self._attr_name = data[CONF_NAME].capitalize() self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) self._offset_value: timedelta | None = None self.entity_id = entity_id self._attr_unique_id = unique_id self._attr_entity_registry_enabled_default = entity_enabled + @property + def should_poll(self) -> bool: + """Enable polling for the entity. + + The coordinator is not used by multiple entities, but instead + is used to poll the calendar API at a separate interval from the + entity state updates itself which happen more frequently (e.g. to + fire an alarm when the next event starts). + """ + return True + @property def extra_state_attributes(self) -> dict[str, bool]: """Return the device state attributes.""" @@ -257,60 +333,50 @@ class GoogleCalendarEntity(CalendarEntity): """Return the next upcoming event.""" return self._event - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - def _event_filter(self, event: Event) -> bool: """Return True if the event is visible.""" if self._ignore_availability: return True return event.transparency == OPAQUE + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._apply_coordinator_update() + async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - - request = ListEventsRequest( - calendar_id=self.calendar_id, - start_time=start_date, - end_time=end_date, - search=self._search, - ) - result_items = [] - try: - result = await self.calendar_service.async_list_events(request) - async for result_page in result: - result_items.extend(result_page.items) - except ApiException as err: - _LOGGER.error("Unable to connect to Google: %s", err) - return [] + result_items = await self.coordinator.async_get_events(start_date, end_date) return [ _get_calendar_event(event) for event in filter(self._event_filter, result_items) ] - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self) -> None: - """Get the latest data.""" - request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) - try: - result = await self.calendar_service.async_list_events(request) - except ApiException as err: - _LOGGER.error("Unable to connect to Google: %s", err) - return + def _apply_coordinator_update(self) -> None: + """Copy state from the coordinator to this entity.""" + events = self.coordinator.data + self._event = _get_calendar_event(next(iter(events))) if events else None + if self._event: + (self._event.summary, self._offset_value) = extract_offset( + self._event.summary, self._offset + ) - # Pick the first visible event and apply offset calculations. - valid_items = filter(self._event_filter, result.items) - event = copy.deepcopy(next(valid_items, None)) - if event: - (event.summary, offset) = extract_offset(event.summary, self._offset) - self._event = _get_calendar_event(event) - self._offset_value = offset - else: - self._event = None + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._apply_coordinator_update() + super()._handle_coordinator_update() + + async def async_update(self) -> None: + """Disable update behavior. + + This relies on the coordinator callback update to write home assistant + state with the next calendar event. This update is a no-op as no new data + fetch is needed to evaluate the state to determine if the next event has + started, handled by CalendarEntity parent class. + """ def _get_calendar_event(event: Event) -> CalendarEvent: @@ -362,12 +428,15 @@ async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) -> if start is None or end is None: raise ValueError("Missing required fields to set start or end date/datetime") - await entity.calendar_service.async_create_event( - entity.calendar_id, - Event( - summary=call.data[EVENT_SUMMARY], - description=call.data[EVENT_DESCRIPTION], - start=start, - end=end, - ), - ) + try: + await entity.coordinator.calendar_service.async_create_event( + entity.calendar_id, + Event( + summary=call.data[EVENT_SUMMARY], + description=call.data[EVENT_DESCRIPTION], + start=start, + end=end, + ), + ) + except ApiException as err: + raise HomeAssistantError(str(err)) from err diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 22b62094e76..e320212ca1b 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -91,6 +91,9 @@ class OAuth2FlowHandler( self.flow_impl.client_secret, calendar_access, ) + except TimeoutError as err: + _LOGGER.error("Timeout initializing device flow: %s", str(err)) + return self.async_abort(reason="timeout_connect") except OAuthError as err: _LOGGER.error("Error initializing device flow: %s", str(err)) return self.async_abort(reason="oauth_error") diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d39f2093cf0..bf745a72927 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -2,7 +2,7 @@ "domain": "google", "name": "Google Calendars", "config_flow": true, - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": ["gcal-sync==0.10.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 3ff75047f70..58e5cedd98d 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -16,6 +16,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "code_expired": "Authentication code expired or credential setup is invalid, please try again.", @@ -40,5 +41,15 @@ }, "application_credentials": { "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" + }, + "issues": { + "deprecated_yaml": { + "title": "The Google Calendar YAML configuration is being removed", + "description": "Configuring the Google Calendar in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_track_new_yaml": { + "title": "Google Calendar entity tracking has changed", + "description": "You have disabled entity tracking for Google Calendar in configuration.yaml, which is no longer supported. You must manually change the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/google/translations/ca.json b/homeassistant/components/google/translations/ca.json index 004fcb67f46..066630df50d 100644 --- a/homeassistant/components/google/translations/ca.json +++ b/homeassistant/components/google/translations/ca.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "oauth_error": "S'han rebut dades token inv\u00e0lides.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" }, "create_entry": { "default": "Autenticaci\u00f3 exitosa" diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index 433324147dd..2e81b2357c8 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -11,7 +11,8 @@ "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "oauth_error": "Ung\u00fcltige Token-Daten empfangen.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" }, "create_entry": { "default": "Erfolgreich authentifiziert" @@ -32,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration des Google Kalenders in configuration.yaml wird in Home Assistant 2022.9 entfernt. \n\nDeine bestehenden OAuth-Anwendungsdaten und Zugriffseinstellungen wurden automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Google Calendar YAML-Konfiguration wird entfernt" + }, + "removed_track_new_yaml": { + "description": "Du hast die Entit\u00e4tsverfolgung f\u00fcr Google Kalender in configuration.yaml deaktiviert, was nicht mehr unterst\u00fctzt wird. Du musst die Integrationssystemoptionen in der Benutzeroberfl\u00e4che manuell \u00e4ndern, um neu entdeckte Entit\u00e4ten in Zukunft zu deaktivieren. Entferne die Einstellung track_new aus configuration.yaml und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Google Calendar Entity Tracking hat sich ge\u00e4ndert" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index 65cc5a0038d..0bf592d60d4 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -11,7 +11,8 @@ "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd.", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "create_entry": { "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" @@ -32,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.9. \n\n \u03a4\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 OAuth \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "removed_track_new_yaml": { + "description": "\u0388\u03c7\u03b5\u03c4\u03b5 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf Google \u03c3\u03c4\u03bf configuration.yaml, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03c4\u03b9\u03c2 \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 track_new \u03b1\u03c0\u03cc \u03c4\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 2ef34ccc84b..4ce207ccd5b 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -11,7 +11,8 @@ "invalid_access_token": "Invalid access token", "missing_configuration": "The component is not configured. Please follow the documentation.", "oauth_error": "Received invalid token data.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "timeout_connect": "Timeout establishing connection" }, "create_entry": { "default": "Successfully authenticated" @@ -32,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Google Calendar in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Google Calendar YAML configuration is being removed" + }, + "removed_track_new_yaml": { + "description": "You have disabled entity tracking for Google Calendar in configuration.yaml, which is no longer supported. You must manually change the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue.", + "title": "Google Calendar entity tracking has changed" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json index 1b5aff5774b..c516c9201e2 100644 --- a/homeassistant/components/google/translations/et.json +++ b/homeassistant/components/google/translations/et.json @@ -11,7 +11,8 @@ "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", "oauth_error": "Saadi sobimatud loaandmed.", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "timeout_connect": "\u00dchenduse loomise ajal\u00f6pp" }, "create_entry": { "default": "Tuvastamine \u00f5nnestus" diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json index 7c5e4927787..389f769cdb9 100644 --- a/homeassistant/components/google/translations/fr.json +++ b/homeassistant/components/google/translations/fr.json @@ -11,7 +11,8 @@ "invalid_access_token": "Jeton d'acc\u00e8s non valide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "oauth_error": "Des donn\u00e9es de jeton non valides ont \u00e9t\u00e9 re\u00e7ues.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9" }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json index 0ff516dcfed..b27e06b15c7 100644 --- a/homeassistant/components/google/translations/hu.json +++ b/homeassistant/components/google/translations/hu.json @@ -7,11 +7,12 @@ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "code_expired": "A hiteles\u00edt\u00e9si k\u00f3d lej\u00e1rt vagy a hiteles\u00edt\u0151 adatok be\u00e1ll\u00edt\u00e1sa \u00e9rv\u00e9nytelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra.", + "code_expired": "A hiteles\u00edt\u00e9si k\u00f3d lej\u00e1rt vagy a hiteles\u00edt\u0151 adatok be\u00e1ll\u00edt\u00e1sa \u00e9rv\u00e9nytelen, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra.", "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "oauth_error": "\u00c9rv\u00e9nytelen token adatok \u00e9rkeztek.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n" }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index ea13f27fce5..20ed21a56be 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token akses tidak valid", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "oauth_error": "Menerima respons token yang tidak valid.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "timeout_connect": "Tenggang waktu membuat koneksi habis" }, "create_entry": { "default": "Berhasil diautentikasi" diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index ef5ec01202d..6e24178996f 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token di accesso non valido", "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "oauth_error": "Ricevuti dati token non validi.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "timeout_connect": "Tempo scaduto per stabile la connessione." }, "create_entry": { "default": "Autenticazione riuscita" @@ -32,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Google Calendar verr\u00e0 rimossa" + }, + "removed_track_new_yaml": { + "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "Il tracciamento dell'entit\u00e0 di Google Calendar \u00e8 cambiato" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 4cb05958a76..6e2aac00c5d 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -11,7 +11,8 @@ "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "oauth_error": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" @@ -27,8 +28,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Nest\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json index 3f78e3e9d6f..fb85af430df 100644 --- a/homeassistant/components/google/translations/pl.json +++ b/homeassistant/components/google/translations/pl.json @@ -11,7 +11,8 @@ "invalid_access_token": "Niepoprawny token dost\u0119pu", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "oauth_error": "Otrzymano nieprawid\u0142owe dane tokena.", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia" }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" @@ -32,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Kalendarza Google w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2022.9. \n\nTwoje istniej\u0105ce po\u015bwiadczenia aplikacji OAuth i ustawienia dost\u0119pu zosta\u0142y automatycznie zaimportowane do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Kalendarza Google zostanie usuni\u0119ta" + }, + "removed_track_new_yaml": { + "description": "Wy\u0142\u0105czy\u0142e\u015b \u015bledzenie encji w Kalendarzu Google w pliku configuration.yaml, kt\u00f3ry nie jest ju\u017c obs\u0142ugiwany. Musisz r\u0119cznie zmieni\u0107 ustawienie w Opcjach Systemu integracji, aby wy\u0142\u0105czy\u0107 nowo wykryte encje w przysz\u0142o\u015bci. Usu\u0144 ustawienie track_new z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "\u015aledzenie encji Kalendarza Google uleg\u0142o zmianie" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index e934155c9fa..0b115c46423 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "oauth_error": "Dados de token recebidos inv\u00e1lidos.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" }, "create_entry": { "default": "Autenticado com sucesso" @@ -32,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Google Agenda em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Google Agenda est\u00e1 sendo removida" + }, + "removed_track_new_yaml": { + "description": "Voc\u00ea desativou as entidades de rastreamento para o Google Agenda em configuration.yaml, que n\u00e3o \u00e9 mais compat\u00edvel. Voc\u00ea deve alterar manualmente as op\u00e7\u00f5es do sistema de integra\u00e7\u00e3o na interface do usu\u00e1rio para desativar as entidades rec\u00e9m-descobertas daqui para frente. Remova a configura\u00e7\u00e3o track_new de configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A entidade de rastreamento do Google Agenda foi alterado" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/pt.json b/homeassistant/components/google/translations/pt.json new file mode 100644 index 00000000000..518809302e3 --- /dev/null +++ b/homeassistant/components/google/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 51badc7226d..57a6791cedf 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -1,13 +1,18 @@ { + "application_credentials": { + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044e Google. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0432\u0430\u0448\u0438\u043c \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u0435\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n3. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **TV and Limited Input devices** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0422\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + }, "config": { "abort": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "code_expired": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043a\u043e\u0434\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0435\u0432\u0435\u0440\u043d\u043e, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." @@ -28,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "removed_track_new_yaml": { + "description": "\u0412\u044b \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u043b\u0438 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f Google Calendar \u0432 \u0444\u0430\u0439\u043b\u0435 configuration.yaml, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0427\u0442\u043e\u0431\u044b \u043d\u043e\u0432\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043d\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u043b\u0438\u0441\u044c \u0432 Home Assistant \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 track_new \u0438\u0437 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 Google Calendar" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/tr.json b/homeassistant/components/google/translations/tr.json index ec265926695..7d67018630f 100644 --- a/homeassistant/components/google/translations/tr.json +++ b/homeassistant/components/google/translations/tr.json @@ -11,7 +11,8 @@ "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131" }, "create_entry": { "default": "Ba\u015far\u0131yla do\u011fruland\u0131" diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index bee271ea8e7..0d2031c368f 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -11,7 +11,8 @@ "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" @@ -32,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + }, + "removed_track_new_yaml": { + "description": "\u65bc configuration.yaml \u5167\u6240\u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u529f\u80fd\uff0c\u7531\u65bc\u4e0d\u518d\u652f\u6301\u3001\u5df2\u7d93\u906d\u5230\u95dc\u9589\u3002\u4e4b\u5f8c\u5fc5\u9808\u624b\u52d5\u900f\u904e\u4ecb\u9762\u5167\u7684\u6574\u5408\u529f\u80fd\u3001\u4ee5\u95dc\u9589\u4efb\u4f55\u65b0\u767c\u73fe\u7684\u5be6\u9ad4\u3002\u8acb\u7531 configuration.yaml \u4e2d\u79fb\u9664R track_new \u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u5df2\u7d93\u8b8a\u66f4" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 6f81ddebdb4..0b9b12f2f4c 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -51,7 +51,7 @@ LOCAL_SDK_MIN_VERSION = AwesomeVersion("2.1.5") @callback def _get_registry_entries( hass: HomeAssistant, entity_id: str -) -> tuple[device_registry.DeviceEntry, area_registry.AreaEntry]: +) -> tuple[device_registry.DeviceEntry | None, area_registry.AreaEntry | None]: """Get registry entries.""" ent_reg = entity_registry.async_get(hass) dev_reg = device_registry.async_get(hass) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 3f3db1f2b5b..84d5e4a3364 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -59,7 +59,7 @@ def _get_homegraph_jwt(time, iss, key): async def _get_homegraph_token( hass: HomeAssistant, jwt_signed: str -) -> dict[str, Any] | list[str, Any] | Any: +) -> dict[str, Any] | list[Any] | Any: headers = { "Authorization": f"Bearer {jwt_signed}", "Content-Type": "application/x-www-form-urlencoded", diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 4e8ac1624cc..737b54c8b1e 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import deque import logging +from typing import Any from homeassistant.const import MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback @@ -28,7 +29,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig """Enable state reporting.""" checker = None unsub_pending: CALLBACK_TYPE | None = None - pending = deque([{}]) + pending: deque[dict[str, Any]] = deque([{}]) async def report_states(now=None): """Report the states.""" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 42fc43197ea..ee41dd0c678 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -176,6 +176,8 @@ def _next_selected(items: list[str], selected: str | None) -> str | None: If selected is missing in items, None is returned """ + if selected is None: + return None try: index = items.index(selected) except ValueError: @@ -188,7 +190,7 @@ def _next_selected(items: list[str], selected: str | None) -> str | None: class _Trait: """Represents a Trait inside Google Assistant skill.""" - commands = [] + commands: list[str] = [] @staticmethod def might_2fa(domain, features, device_class): @@ -1701,7 +1703,7 @@ class InputSelectorTrait(_Trait): name = TRAIT_INPUTSELECTOR commands = [COMMAND_INPUT, COMMAND_NEXT_INPUT, COMMAND_PREVIOUS_INPUT] - SYNONYMS = {} + SYNONYMS: dict[str, list[str]] = {} @staticmethod def supported(domain, features, device_class, _): @@ -2197,7 +2199,7 @@ class MediaStateTrait(_Trait): """ name = TRAIT_MEDIA_STATE - commands = [] + commands: list[str] = [] activity_lookup = { STATE_OFF: "INACTIVE", @@ -2314,7 +2316,7 @@ class SensorStateTrait(_Trait): } name = TRAIT_SENSOR_STATE - commands = [] + commands: list[str] = [] @classmethod def supported(cls, domain, features, device_class, _): diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 633c5edc453..d48571c55bd 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==2.11.1"], + "requirements": ["google-cloud-texttospeech==2.12.0"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 2012e38e0a2..7d125be5025 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/google_travel_time/translations/cs.json b/homeassistant/components/google_travel_time/translations/cs.json index a6c3b361960..19da83d1596 100644 --- a/homeassistant/components/google_travel_time/translations/cs.json +++ b/homeassistant/components/google_travel_time/translations/cs.json @@ -19,6 +19,7 @@ "step": { "init": { "data": { + "language": "Jazyk", "time": "\u010cas", "units": "Jednotky" } diff --git a/homeassistant/components/google_travel_time/translations/pt.json b/homeassistant/components/google_travel_time/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py new file mode 100644 index 00000000000..7a134e43ace --- /dev/null +++ b/homeassistant/components/govee_ble/__init__.py @@ -0,0 +1,45 @@ +"""The Govee Bluetooth BLE integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Govee BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py new file mode 100644 index 00000000000..1e3a5566bfd --- /dev/null +++ b/homeassistant/components/govee_ble/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for govee ble integration.""" +from __future__ import annotations + +from typing import Any + +from govee_ble import GoveeBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for govee.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/govee_ble/const.py b/homeassistant/components/govee_ble/const.py new file mode 100644 index 00000000000..4f30ee5023f --- /dev/null +++ b/homeassistant/components/govee_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the Govee Bluetooth integration.""" + +DOMAIN = "govee_ble" diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json new file mode 100644 index 00000000000..624a38ebe9d --- /dev/null +++ b/homeassistant/components/govee_ble/manifest.json @@ -0,0 +1,31 @@ +{ + "domain": "govee_ble", + "name": "Govee Bluetooth", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/govee_ble", + "bluetooth": [ + { "local_name": "Govee*" }, + { "local_name": "GVH5*" }, + { "local_name": "B5178*" }, + { + "manufacturer_id": 26589, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 18994, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 14474, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" + } + ], + "requirements": ["govee-ble==0.12.6"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py new file mode 100644 index 00000000000..d0b9447d9e3 --- /dev/null +++ b/homeassistant/components/govee_ble/sensor.py @@ -0,0 +1,157 @@ +"""Support for govee ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from govee_ble import ( + DeviceClass, + DeviceKey, + GoveeBluetoothDeviceData, + SensorDeviceInfo, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to hass device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Govee BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + data = GoveeBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload( + processor.async_add_entities_listener( + GoveeBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class GoveeBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a govee ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/govee_ble/strings.json b/homeassistant/components/govee_ble/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/govee_ble/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "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%]" + } + } +} diff --git a/homeassistant/components/govee_ble/translations/ca.json b/homeassistant/components/govee_ble/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/govee_ble/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/de.json b/homeassistant/components/govee_ble/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/el.json b/homeassistant/components/govee_ble/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/en.json b/homeassistant/components/govee_ble/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/et.json b/homeassistant/components/govee_ble/translations/et.json new file mode 100644 index 00000000000..8dc1b9f6ed0 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/fr.json b/homeassistant/components/govee_ble/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/govee_ble/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/hu.json b/homeassistant/components/govee_ble/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/id.json b/homeassistant/components/govee_ble/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/it.json b/homeassistant/components/govee_ble/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/ja.json b/homeassistant/components/govee_ble/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/pl.json b/homeassistant/components/govee_ble/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/pt-BR.json b/homeassistant/components/govee_ble/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/ru.json b/homeassistant/components/govee_ble/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/zh-Hant.json b/homeassistant/components/govee_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/govee_ble/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpmdp/__init__.py b/homeassistant/components/gpmdp/__init__.py deleted file mode 100644 index a8aa82c69c3..00000000000 --- a/homeassistant/components/gpmdp/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The gpmdp component.""" diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json deleted file mode 100644 index 51fad8e9e71..00000000000 --- a/homeassistant/components/gpmdp/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "gpmdp", - "name": "Google Play Music Desktop Player (GPMDP)", - "disabled": "Integration has incompatible requirements.", - "documentation": "https://www.home-assistant.io/integrations/gpmdp", - "requirements": ["websocket-client==0.54.0"], - "dependencies": ["configurator"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py deleted file mode 100644 index b2861e4d96d..00000000000 --- a/homeassistant/components/gpmdp/media_player.py +++ /dev/null @@ -1,382 +0,0 @@ -"""Support for Google Play Music Desktop Player.""" -from __future__ import annotations - -import json -import logging -import socket -import time -from typing import Any - -import voluptuous as vol -from websocket import _exceptions, create_connection - -from homeassistant.components import configurator -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, - MediaPlayerEntity, - MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.json import load_json, save_json - -_CONFIGURING: dict[str, Any] = {} -_LOGGER = logging.getLogger(__name__) - -DEFAULT_HOST = "localhost" -DEFAULT_NAME = "GPM Desktop Player" -DEFAULT_PORT = 5672 - -GPMDP_CONFIG_FILE = "gpmpd.conf" - -PLAYBACK_DICT = {"0": STATE_PAUSED, "1": STATE_PAUSED, "2": STATE_PLAYING} # Stopped - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } -) - - -def request_configuration(hass, config, url, add_entities_callback): - """Request configuration steps from the user.""" - if "gpmdp" in _CONFIGURING: - configurator.notify_errors( - hass, _CONFIGURING["gpmdp"], "Failed to register, please try again." - ) - - return - websocket = create_connection((url), timeout=1) - websocket.send( - json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant"], - } - ) - ) - - def gpmdp_configuration_callback(callback_data): - """Handle configuration changes.""" - while True: - - try: - msg = json.loads(websocket.recv()) - except _exceptions.WebSocketConnectionClosedException: - continue - if msg["channel"] != "connect": - continue - if msg["payload"] != "CODE_REQUIRED": - continue - pin = callback_data.get("pin") - websocket.send( - json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant", pin], - } - ) - ) - tmpmsg = json.loads(websocket.recv()) - if tmpmsg["channel"] == "time": - _LOGGER.error( - "Error setting up GPMDP. Please pause " - "the desktop player and try again" - ) - break - if (code := tmpmsg["payload"]) == "CODE_REQUIRED": - continue - setup_gpmdp(hass, config, code, add_entities_callback) - save_json(hass.config.path(GPMDP_CONFIG_FILE), {"CODE": code}) - websocket.send( - json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant", code], - } - ) - ) - websocket.close() - break - - _CONFIGURING["gpmdp"] = configurator.request_config( - DEFAULT_NAME, - gpmdp_configuration_callback, - description=( - "Enter the pin that is displayed in the " - "Google Play Music Desktop Player." - ), - submit_caption="Submit", - fields=[{"id": "pin", "name": "Pin Code", "type": "number"}], - ) - - -def setup_gpmdp(hass, config, code, add_entities): - """Set up gpmdp.""" - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - url = f"ws://{host}:{port}" - - if not code: - request_configuration(hass, config, url, add_entities) - return - - if "gpmdp" in _CONFIGURING: - configurator.request_done(hass, _CONFIGURING.pop("gpmdp")) - - add_entities([GPMDP(name, url, code)], True) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the GPMDP platform.""" - codeconfig = load_json(hass.config.path(GPMDP_CONFIG_FILE)) - if codeconfig: - code = codeconfig.get("CODE") if isinstance(codeconfig, dict) else None - elif discovery_info is not None: - if "gpmdp" in _CONFIGURING: - return - code = None - else: - code = None - setup_gpmdp(hass, config, code, add_entities) - - -class GPMDP(MediaPlayerEntity): - """Representation of a GPMDP.""" - - _attr_supported_features = ( - MediaPlayerEntityFeature.PAUSE - | MediaPlayerEntityFeature.PREVIOUS_TRACK - | MediaPlayerEntityFeature.NEXT_TRACK - | MediaPlayerEntityFeature.SEEK - | MediaPlayerEntityFeature.VOLUME_SET - | MediaPlayerEntityFeature.PLAY - ) - - def __init__(self, name, url, code): - """Initialize the media player.""" - - self._connection = create_connection - self._url = url - self._authorization_code = code - self._name = name - self._status = STATE_OFF - self._ws = None - self._title = None - self._artist = None - self._albumart = None - self._seek_position = None - self._duration = None - self._volume = None - self._request_id = 0 - self._available = True - - def get_ws(self): - """Check if the websocket is setup and connected.""" - if self._ws is None: - try: - self._ws = self._connection((self._url), timeout=1) - msg = json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant", self._authorization_code], - } - ) - self._ws.send(msg) - except (socket.timeout, ConnectionRefusedError, ConnectionResetError): - self._ws = None - return self._ws - - def send_gpmdp_msg(self, namespace, method, with_id=True): - """Send ws messages to GPMDP and verify request id in response.""" - - try: - if (websocket := self.get_ws()) is None: - self._status = STATE_OFF - return - self._request_id += 1 - websocket.send( - json.dumps( - { - "namespace": namespace, - "method": method, - "requestID": self._request_id, - } - ) - ) - if not with_id: - return - while True: - msg = json.loads(websocket.recv()) - if "requestID" in msg and msg["requestID"] == self._request_id: - return msg - except ( - ConnectionRefusedError, - ConnectionResetError, - _exceptions.WebSocketTimeoutException, - _exceptions.WebSocketProtocolException, - _exceptions.WebSocketPayloadException, - _exceptions.WebSocketConnectionClosedException, - ): - self._ws = None - - def update(self): - """Get the latest details from the player.""" - time.sleep(1) - try: - self._available = True - playstate = self.send_gpmdp_msg("playback", "getPlaybackState") - if playstate is None: - return - self._status = PLAYBACK_DICT[str(playstate["value"])] - time_data = self.send_gpmdp_msg("playback", "getCurrentTime") - if time_data is not None: - self._seek_position = int(time_data["value"] / 1000) - track_data = self.send_gpmdp_msg("playback", "getCurrentTrack") - if track_data is not None: - self._title = track_data["value"]["title"] - self._artist = track_data["value"]["artist"] - self._albumart = track_data["value"]["albumArt"] - self._duration = int(track_data["value"]["duration"] / 1000) - volume_data = self.send_gpmdp_msg("volume", "getVolume") - if volume_data is not None: - self._volume = volume_data["value"] / 100 - except OSError: - self._available = False - - @property - def available(self): - """Return if media player is available.""" - return self._available - - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - def state(self): - """Return the state of the device.""" - return self._status - - @property - def media_title(self): - """Title of current playing media.""" - return self._title - - @property - def media_artist(self): - """Artist of current playing media (Music track only).""" - return self._artist - - @property - def media_image_url(self): - """Image url of current playing media.""" - return self._albumart - - @property - def media_seek_position(self): - """Time in seconds of current seek position.""" - return self._seek_position - - @property - def media_duration(self): - """Time in seconds of current song duration.""" - return self._duration - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def name(self): - """Return the name of the device.""" - return self._name - - def media_next_track(self): - """Send media_next command to media player.""" - self.send_gpmdp_msg("playback", "forward", False) - - def media_previous_track(self): - """Send media_previous command to media player.""" - self.send_gpmdp_msg("playback", "rewind", False) - - def media_play(self): - """Send media_play command to media player.""" - self.send_gpmdp_msg("playback", "playPause", False) - self._status = STATE_PLAYING - self.schedule_update_ha_state() - - def media_pause(self): - """Send media_pause command to media player.""" - self.send_gpmdp_msg("playback", "playPause", False) - self._status = STATE_PAUSED - self.schedule_update_ha_state() - - def media_seek(self, position): - """Send media_seek command to media player.""" - if (websocket := self.get_ws()) is None: - return - websocket.send( - json.dumps( - { - "namespace": "playback", - "method": "setCurrentTime", - "arguments": [position * 1000], - } - ) - ) - self.schedule_update_ha_state() - - def volume_up(self): - """Send volume_up command to media player.""" - if (websocket := self.get_ws()) is None: - return - websocket.send('{"namespace": "volume", "method": "increaseVolume"}') - self.schedule_update_ha_state() - - def volume_down(self): - """Send volume_down command to media player.""" - if (websocket := self.get_ws()) is None: - return - websocket.send('{"namespace": "volume", "method": "decreaseVolume"}') - self.schedule_update_ha_state() - - def set_volume_level(self, volume): - """Set volume on media player, range(0..1).""" - if (websocket := self.get_ws()) is None: - return - websocket.send( - json.dumps( - { - "namespace": "volume", - "method": "setVolume", - "arguments": [volume * 100], - } - ) - ) - self.schedule_update_ha_state() diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 3df0bac51e9..5331f6e7029 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/gpslogger/translations/ja.json b/homeassistant/components/gpslogger/translations/ja.json index 4674c074763..6b05591da9b 100644 --- a/homeassistant/components/gpslogger/translations/ja.json +++ b/homeassistant/components/gpslogger/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index fa51a48bb4f..d4a929f1642 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery hass.data[DOMAIN].setdefault(DISPATCHERS, []) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def _async_scan_update(_=None): await gree_discovery.discovery.scan() diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 0d1c7d53f8b..2b6833dff2c 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -198,14 +198,14 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return TARGET_TEMPERATURE_STEP @property - def hvac_mode(self) -> str: + def hvac_mode(self) -> HVACMode | None: """Return the current HVAC mode for the device.""" if not self.coordinator.device.power: return HVACMode.OFF return HVAC_MODES.get(self.coordinator.device.mode) - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Invalid hvac_mode: {hvac_mode}") @@ -246,7 +246,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): self.async_write_ha_state() @property - def hvac_modes(self) -> list[str]: + def hvac_modes(self) -> list[HVACMode]: """Return the HVAC modes support by the device.""" modes = [*HVAC_MODES_REVERSE] modes.append(HVACMode.OFF) @@ -299,7 +299,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return PRESET_MODES @property - def fan_mode(self) -> str: + def fan_mode(self) -> str | None: """Return the current fan mode for the device.""" speed = self.coordinator.device.fan_speed return FAN_MODES.get(speed) diff --git a/homeassistant/components/gree/translations/ja.json b/homeassistant/components/gree/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/gree/translations/ja.json +++ b/homeassistant/components/gree/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 1f8fba21e78..04bc109d15b 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -231,7 +231,9 @@ def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - hass.config_entries.async_setup_platforms(entry, (entry.options["group_type"],)) + await hass.config_entries.async_forward_entry_setups( + entry, (entry.options["group_type"],) + ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True @@ -572,7 +574,6 @@ class Group(Entity): return group @staticmethod - @callback async def async_create_group( hass: HomeAssistant, name: str, diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 940aa088ced..0d0e11d7ffe 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -1,10 +1,36 @@ { "config": { "step": { + "binary_sensor": { + "title": "Adicionar Grupo" + }, + "fan": { + "title": "Adicionar Grupo" + }, + "light": { + "title": "Adicionar Grupo" + }, "media_player": { "data": { - "entities": "Membros", - "name": "Nome" + "entities": "Membros" + }, + "title": "Adicionar Grupo" + }, + "switch": { + "title": "Adicionar Grupo" + } + } + }, + "options": { + "step": { + "fan": { + "data": { + "hide_members": "Esconder membros" + } + }, + "switch": { + "data": { + "hide_members": "Esconder membros" } } } diff --git a/homeassistant/components/growatt_server/__init__.py b/homeassistant/components/growatt_server/__init__.py index f77323bf536..177d0957883 100644 --- a/homeassistant/components/growatt_server/__init__.py +++ b/homeassistant/components/growatt_server/__init__.py @@ -11,7 +11,7 @@ async def async_setup_entry( ) -> bool: """Load the saved entities.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/growatt_server/translations/pt.json b/homeassistant/components/growatt_server/translations/pt.json new file mode 100644 index 00000000000..a814cc1772d --- /dev/null +++ b/homeassistant/components/growatt_server/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 25e0df913d8..58d70667cdf 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable +from dataclasses import dataclass from typing import cast from aioguardian import Client @@ -24,10 +25,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( API_SENSOR_PAIR_DUMP, @@ -37,9 +35,6 @@ from .const import ( API_VALVE_STATUS, API_WIFI_STATUS, CONF_UID, - DATA_CLIENT, - DATA_COORDINATOR, - DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN, LOGGER, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -88,8 +83,17 @@ SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( }, ) +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] + +@dataclass +class GuardianData: + """Define an object to be stored in `hass.data`.""" + + entry: ConfigEntry + client: Client + valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] + paired_sensor_manager: PairedSensorManager @callback @@ -106,6 +110,25 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) raise ValueError(f"No client for device ID: {device_id}") +@callback +def async_log_deprecated_service_call( + hass: HomeAssistant, + call: ServiceCall, + alternate_service: str, + alternate_target: str, +) -> None: + """Log a warning about a deprecated service call.""" + LOGGER.warning( + ( + 'The "%s" service is deprecated and will be removed in a future version; ' + 'use the "%s" service and pass it a target entity ID of "%s"' + ), + f"{call.domain}.{call.service}", + alternate_service, + alternate_target, + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) @@ -114,8 +137,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # so we use a lock to ensure that only one API request is reaching it at a time: api_lock = asyncio.Lock() - # Set up DataUpdateCoordinators for the valve controller: - coordinators: dict[str, GuardianDataUpdateCoordinator] = {} + async def async_init_coordinator( + coordinator: GuardianDataUpdateCoordinator, + ) -> None: + """Initialize a GuardianDataUpdateCoordinator.""" + await coordinator.async_initialize() + await coordinator.async_config_entry_first_refresh() + + # Set up GuardianDataUpdateCoordinators for the valve controller: + valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] for api, api_coro in ( (API_SENSOR_PAIR_DUMP, client.sensor.pair_dump), @@ -124,57 +154,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (API_VALVE_STATUS, client.valve.status), (API_WIFI_STATUS, client.wifi.status), ): - coordinator = coordinators[api] = GuardianDataUpdateCoordinator( + coordinator = valve_controller_coordinators[ + api + ] = GuardianDataUpdateCoordinator( hass, + entry=entry, client=client, api_name=api, api_coro=api_coro, api_lock=api_lock, valve_controller_uid=entry.data[CONF_UID], ) - init_valve_controller_tasks.append(coordinator.async_refresh()) + init_valve_controller_tasks.append(async_init_coordinator(coordinator)) await asyncio.gather(*init_valve_controller_tasks) # Set up an object to evaluate each batch of paired sensor UIDs and add/remove # devices as appropriate: - paired_sensor_manager = PairedSensorManager(hass, entry, client, api_lock) - await paired_sensor_manager.async_process_latest_paired_sensor_uids() + paired_sensor_manager = PairedSensorManager( + hass, + entry, + client, + api_lock, + valve_controller_coordinators[API_SENSOR_PAIR_DUMP], + ) + await paired_sensor_manager.async_initialize() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_CLIENT: client, - DATA_COORDINATOR: coordinators, - DATA_COORDINATOR_PAIRED_SENSOR: {}, - DATA_PAIRED_SENSOR_MANAGER: paired_sensor_manager, - } - - @callback - def async_process_paired_sensor_uids() -> None: - """Define a callback for when new paired sensor data is received.""" - hass.async_create_task( - paired_sensor_manager.async_process_latest_paired_sensor_uids() - ) - - coordinators[API_SENSOR_PAIR_DUMP].async_add_listener( - async_process_paired_sensor_uids + hass.data[DOMAIN][entry.entry_id] = GuardianData( + entry=entry, + client=client, + valve_controller_coordinators=valve_controller_coordinators, + paired_sensor_manager=paired_sensor_manager, ) # Set up all of the Guardian entity platforms: - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback - def extract_client(func: Callable) -> Callable: - """Define a decorator to get the correct client for a service call.""" + def call_with_data(func: Callable) -> Callable: + """Hydrate a service call with the appropriate GuardianData object.""" async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" entry_id = async_get_entry_id_for_service_call(hass, call) - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + data = hass.data[DOMAIN][entry_id] try: - async with client: - await func(call, client) + async with data.client: + await func(call, data) except GuardianError as err: raise HomeAssistantError( f"Error while executing {func.__name__}: {err}" @@ -182,50 +210,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper - @extract_client - async def async_disable_ap(call: ServiceCall, client: Client) -> None: + @call_with_data + async def async_disable_ap(call: ServiceCall, data: GuardianData) -> None: """Disable the onboard AP.""" - await client.wifi.disable_ap() + await data.client.wifi.disable_ap() - @extract_client - async def async_enable_ap(call: ServiceCall, client: Client) -> None: + @call_with_data + async def async_enable_ap(call: ServiceCall, data: GuardianData) -> None: """Enable the onboard AP.""" - await client.wifi.enable_ap() + await data.client.wifi.enable_ap() - @extract_client - async def async_pair_sensor(call: ServiceCall, client: Client) -> None: + @call_with_data + async def async_pair_sensor(call: ServiceCall, data: GuardianData) -> None: """Add a new paired sensor.""" - entry_id = async_get_entry_id_for_service_call(hass, call) - paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] uid = call.data[CONF_UID] + await data.client.sensor.pair_sensor(uid) + await data.paired_sensor_manager.async_pair_sensor(uid) - await client.sensor.pair_sensor(uid) - await paired_sensor_manager.async_pair_sensor(uid) - - @extract_client - async def async_reboot(call: ServiceCall, client: Client) -> None: + @call_with_data + async def async_reboot(call: ServiceCall, data: GuardianData) -> None: """Reboot the valve controller.""" - await client.system.reboot() + async_log_deprecated_service_call( + hass, + call, + "button.press", + f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reboot", + ) + await data.client.system.reboot() - @extract_client - async def async_reset_valve_diagnostics(call: ServiceCall, client: Client) -> None: + @call_with_data + async def async_reset_valve_diagnostics( + call: ServiceCall, data: GuardianData + ) -> None: """Fully reset system motor diagnostics.""" - await client.valve.reset() + async_log_deprecated_service_call( + hass, + call, + "button.press", + f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reset_valve_diagnostics", + ) + await data.client.valve.reset() - @extract_client - async def async_unpair_sensor(call: ServiceCall, client: Client) -> None: + @call_with_data + async def async_unpair_sensor(call: ServiceCall, data: GuardianData) -> None: """Remove a paired sensor.""" - entry_id = async_get_entry_id_for_service_call(hass, call) - paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] uid = call.data[CONF_UID] + await data.client.sensor.unpair_sensor(uid) + await data.paired_sensor_manager.async_unpair_sensor(uid) - await client.sensor.unpair_sensor(uid) - await paired_sensor_manager.async_unpair_sensor(uid) - - @extract_client - async def async_upgrade_firmware(call: ServiceCall, client: Client) -> None: + @call_with_data + async def async_upgrade_firmware(call: ServiceCall, data: GuardianData) -> None: """Upgrade the device firmware.""" - await client.system.upgrade_firmware( + await data.client.system.upgrade_firmware( url=call.data[CONF_URL], port=call.data[CONF_PORT], filename=call.data[CONF_FILENAME], @@ -292,6 +328,7 @@ class PairedSensorManager: entry: ConfigEntry, client: Client, api_lock: asyncio.Lock, + sensor_pair_dump_coordinator: GuardianDataUpdateCoordinator, ) -> None: """Initialize.""" self._api_lock = api_lock @@ -299,6 +336,21 @@ class PairedSensorManager: self._entry = entry self._hass = hass self._paired_uids: set[str] = set() + self._sensor_pair_dump_coordinator = sensor_pair_dump_coordinator + self.coordinators: dict[str, GuardianDataUpdateCoordinator] = {} + + async def async_initialize(self) -> None: + """Initialize the manager.""" + + @callback + def async_create_process_task() -> None: + """Define a callback for when new paired sensor data is received.""" + self._hass.async_create_task(self.async_process_latest_paired_sensor_uids()) + + cancel_process_task = self._sensor_pair_dump_coordinator.async_add_listener( + async_create_process_task + ) + self._entry.async_on_unload(cancel_process_task) async def async_pair_sensor(self, uid: str) -> None: """Add a new paired sensor coordinator.""" @@ -306,10 +358,9 @@ class PairedSensorManager: self._paired_uids.add(uid) - coordinator = self._hass.data[DOMAIN][self._entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ][uid] = GuardianDataUpdateCoordinator( + coordinator = self.coordinators[uid] = GuardianDataUpdateCoordinator( self._hass, + entry=self._entry, client=self._client, api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}", api_coro=lambda: cast( @@ -329,11 +380,7 @@ class PairedSensorManager: async def async_process_latest_paired_sensor_uids(self) -> None: """Process a list of new UIDs.""" try: - uids = set( - self._hass.data[DOMAIN][self._entry.entry_id][DATA_COORDINATOR][ - API_SENSOR_PAIR_DUMP - ].data["paired_uids"] - ) + uids = set(self._sensor_pair_dump_coordinator.data["paired_uids"]) except KeyError: # Sometimes the paired_uids key can fail to exist; the user can't do anything # about it, so in this case, we quietly abort and return: @@ -357,9 +404,7 @@ class PairedSensorManager: # Clear out objects related to this paired sensor: self._paired_uids.remove(uid) - self._hass.data[DOMAIN][self._entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ].pop(uid) + self.coordinators.pop(uid) # Remove the paired sensor device from the device registry (which will # clean up entities and the entity registry): @@ -370,25 +415,37 @@ class PairedSensorManager: dev_reg.async_remove_device(device.id) -class GuardianEntity(CoordinatorEntity): +class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]): """Define a base Guardian entity.""" - def __init__( # pylint: disable=super-init-not-called - self, entry: ConfigEntry, description: EntityDescription + _attr_has_entity_name = True + + def __init__( + self, coordinator: GuardianDataUpdateCoordinator, description: EntityDescription ) -> None: """Initialize.""" - self._attr_device_info = DeviceInfo(manufacturer="Elexa") + super().__init__(coordinator) + self._attr_extra_state_attributes = {} - self._entry = entry self.entity_description = description @callback def _async_update_from_latest_data(self) -> None: - """Update the entity. + """Update the entity's underlying data. This should be extended by Guardian platforms. """ - raise NotImplementedError + + @callback + def _handle_coordinator_update(self) -> None: + """Respond to a DataUpdateCoordinator update.""" + self._async_update_from_latest_data() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._async_update_from_latest_data() class PairedSensorEntity(GuardianEntity): @@ -397,27 +454,35 @@ class PairedSensorEntity(GuardianEntity): def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, + coordinator: GuardianDataUpdateCoordinator, description: EntityDescription, ) -> None: """Initialize.""" - super().__init__(entry, description) + super().__init__(coordinator, description) paired_sensor_uid = coordinator.data["uid"] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, paired_sensor_uid)}, - name=f"Guardian Paired Sensor {paired_sensor_uid}", + manufacturer="Elexa", + model=coordinator.data["codename"], + name=f"Guardian paired sensor {paired_sensor_uid}", via_device=(DOMAIN, entry.data[CONF_UID]), ) - self._attr_name = ( - f"Guardian Paired Sensor {paired_sensor_uid}: {description.name}" - ) self._attr_unique_id = f"{paired_sensor_uid}_{description.key}" - self.coordinator = coordinator - async def async_added_to_hass(self) -> None: - """Perform tasks when the entity is added.""" - self._async_update_from_latest_data() + +@dataclass +class ValveControllerEntityDescriptionMixin: + """Define an entity description mixin for valve controller entities.""" + + api_category: str + + +@dataclass +class ValveControllerEntityDescription( + EntityDescription, ValveControllerEntityDescriptionMixin +): + """Describe a Guardian valve controller entity.""" class ValveControllerEntity(GuardianEntity): @@ -426,65 +491,18 @@ class ValveControllerEntity(GuardianEntity): def __init__( self, entry: ConfigEntry, - coordinators: dict[str, DataUpdateCoordinator], - description: EntityDescription, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerEntityDescription, ) -> None: """Initialize.""" - super().__init__(entry, description) + super().__init__(coordinators[description.api_category], description) + + self._diagnostics_coordinator = coordinators[API_SYSTEM_DIAGNOSTICS] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.data[CONF_UID])}, - model=coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], - name=f"Guardian Valve Controller {entry.data[CONF_UID]}", + manufacturer="Elexa", + model=self._diagnostics_coordinator.data["firmware"], + name=f"Guardian valve controller {entry.data[CONF_UID]}", ) - self._attr_name = f"Guardian {entry.data[CONF_UID]}: {description.name}" self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" - self.coordinators = coordinators - - @property - def available(self) -> bool: - """Return if entity is available.""" - return any( - coordinator.last_update_success - for coordinator in self.coordinators.values() - ) - - async def _async_continue_entity_setup(self) -> None: - """Perform additional, internal tasks when the entity is about to be added. - - This should be extended by Guardian platforms. - """ - raise NotImplementedError - - @callback - def async_add_coordinator_update_listener(self, api: str) -> None: - """Add a listener to a DataUpdateCoordinator based on the API referenced.""" - - @callback - def update() -> None: - """Update the entity's state.""" - self._async_update_from_latest_data() - self.async_write_ha_state() - - self.async_on_remove(self.coordinators[api].async_add_listener(update)) - - async def async_added_to_hass(self) -> None: - """Perform tasks when the entity is added.""" - await self._async_continue_entity_setup() - self.async_add_coordinator_update_listener(API_SYSTEM_DIAGNOSTICS) - self._async_update_from_latest_data() - - async def async_update(self) -> None: - """Update the entity. - - Only used by the generic entity update service. - """ - # Ignore manual update requests if the entity is disabled - if not self.enabled: - return - - refresh_tasks = [ - coordinator.async_request_refresh() - for coordinator in self.coordinators.values() - ] - await asyncio.gather(*refresh_tasks) diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 1d7195a8f17..766e5d961e8 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -1,6 +1,8 @@ """Binary sensors for the Elexa Guardian integration.""" from __future__ import annotations +from dataclasses import dataclass + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -11,18 +13,21 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import PairedSensorEntity, ValveControllerEntity +from . import ( + GuardianData, + PairedSensorEntity, + ValveControllerEntity, + ValveControllerEntityDescription, +) from .const import ( API_SYSTEM_ONBOARD_SENSOR_STATUS, API_WIFI_STATUS, CONF_UID, - DATA_COORDINATOR, - DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) +from .util import GuardianDataUpdateCoordinator ATTR_CONNECTED_CLIENTS = "connected_clients" @@ -30,31 +35,42 @@ SENSOR_KIND_AP_INFO = "ap_enabled" SENSOR_KIND_LEAK_DETECTED = "leak_detected" SENSOR_KIND_MOVED = "moved" -SENSOR_DESCRIPTION_AP_ENABLED = BinarySensorEntityDescription( - key=SENSOR_KIND_AP_INFO, - name="Onboard AP Enabled", - device_class=BinarySensorDeviceClass.CONNECTIVITY, - entity_category=EntityCategory.DIAGNOSTIC, -) -SENSOR_DESCRIPTION_LEAK_DETECTED = BinarySensorEntityDescription( - key=SENSOR_KIND_LEAK_DETECTED, - name="Leak Detected", - device_class=BinarySensorDeviceClass.MOISTURE, -) -SENSOR_DESCRIPTION_MOVED = BinarySensorEntityDescription( - key=SENSOR_KIND_MOVED, - name="Recently Moved", - device_class=BinarySensorDeviceClass.MOVING, - entity_category=EntityCategory.DIAGNOSTIC, -) + +@dataclass +class ValveControllerBinarySensorDescription( + BinarySensorEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller binary sensor.""" + PAIRED_SENSOR_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_LEAK_DETECTED, - SENSOR_DESCRIPTION_MOVED, + BinarySensorEntityDescription( + key=SENSOR_KIND_LEAK_DETECTED, + name="Leak detected", + device_class=BinarySensorDeviceClass.MOISTURE, + ), + BinarySensorEntityDescription( + key=SENSOR_KIND_MOVED, + name="Recently moved", + device_class=BinarySensorDeviceClass.MOVING, + entity_category=EntityCategory.DIAGNOSTIC, + ), ) + VALVE_CONTROLLER_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_AP_ENABLED, - SENSOR_DESCRIPTION_LEAK_DETECTED, + ValveControllerBinarySensorDescription( + key=SENSOR_KIND_LEAK_DETECTED, + name="Leak detected", + device_class=BinarySensorDeviceClass.MOISTURE, + api_category=API_SYSTEM_ONBOARD_SENSOR_STATUS, + ), + ValveControllerBinarySensorDescription( + key=SENSOR_KIND_AP_INFO, + name="Onboard AP enabled", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + api_category=API_WIFI_STATUS, + ), ) @@ -62,19 +78,16 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" + data: GuardianData = hass.data[DOMAIN][entry.entry_id] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ - uid - ] - async_add_entities( - [ - PairedSensorBinarySensor(entry, coordinator, description) - for description in PAIRED_SENSOR_DESCRIPTIONS - ] + PairedSensorBinarySensor( + entry, data.paired_sensor_manager.coordinators[uid], description + ) + for description in PAIRED_SENSOR_DESCRIPTIONS ) # Handle adding paired sensors after HASS startup: @@ -89,7 +102,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [ ValveControllerBinarySensor( - entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description + entry, data.valve_controller_coordinators, description ) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -98,9 +111,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorBinarySensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ].values() + for coordinator in data.paired_sensor_manager.coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) @@ -111,10 +122,12 @@ async def async_setup_entry( class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): """Define a binary sensor related to a Guardian valve controller.""" + entity_description: BinarySensorEntityDescription + def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, + coordinator: GuardianDataUpdateCoordinator, description: BinarySensorEntityDescription, ) -> None: """Initialize.""" @@ -124,7 +137,7 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: self._attr_is_on = self.coordinator.data["wet"] elif self.entity_description.key == SENSOR_KIND_MOVED: @@ -134,45 +147,26 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): """Define a binary sensor related to a Guardian valve controller.""" + entity_description: ValveControllerBinarySensorDescription + def __init__( self, entry: ConfigEntry, - coordinators: dict[str, DataUpdateCoordinator], - description: BinarySensorEntityDescription, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerBinarySensorDescription, ) -> None: """Initialize.""" super().__init__(entry, coordinators, description) self._attr_is_on = True - async def _async_continue_entity_setup(self) -> None: - """Add an API listener.""" - if self.entity_description.key == SENSOR_KIND_AP_INFO: - self.async_add_coordinator_update_listener(API_WIFI_STATUS) - elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: - self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS) - @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self.entity_description.key == SENSOR_KIND_AP_INFO: - self._attr_available = self.coordinators[ - API_WIFI_STATUS - ].last_update_success - self._attr_is_on = self.coordinators[API_WIFI_STATUS].data[ - "station_connected" - ] - self._attr_extra_state_attributes.update( - { - ATTR_CONNECTED_CLIENTS: self.coordinators[API_WIFI_STATUS].data.get( - "ap_clients" - ) - } - ) + self._attr_is_on = self.coordinator.data["station_connected"] + self._attr_extra_state_attributes[ + ATTR_CONNECTED_CLIENTS + ] = self.coordinator.data.get("ap_clients") elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: - self._attr_available = self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].last_update_success - self._attr_is_on = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ - "wet" - ] + self._attr_is_on = self.coordinator.data["wet"] diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py new file mode 100644 index 00000000000..740cce43c62 --- /dev/null +++ b/homeassistant/components/guardian/button.py @@ -0,0 +1,116 @@ +"""Buttons for the Elexa Guardian integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from aioguardian import Client +from aioguardian.errors import GuardianError + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription +from .const import API_SYSTEM_DIAGNOSTICS, DOMAIN + + +@dataclass +class GuardianButtonEntityDescriptionMixin: + """Define an mixin for button entities.""" + + push_action: Callable[[Client], Awaitable] + + +@dataclass +class ValveControllerButtonDescription( + ButtonEntityDescription, + ValveControllerEntityDescription, + GuardianButtonEntityDescriptionMixin, +): + """Describe a Guardian valve controller button.""" + + +BUTTON_KIND_REBOOT = "reboot" +BUTTON_KIND_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" + + +async def _async_reboot(client: Client) -> None: + """Reboot the Guardian.""" + await client.system.reboot() + + +async def _async_valve_reset(client: Client) -> None: + """Reset the valve diagnostics on the Guardian.""" + await client.valve.reset() + + +BUTTON_DESCRIPTIONS = ( + ValveControllerButtonDescription( + key=BUTTON_KIND_REBOOT, + name="Reboot", + push_action=_async_reboot, + # Buttons don't actually need a coordinator; we give them one so they can + # properly inherit from GuardianEntity: + api_category=API_SYSTEM_DIAGNOSTICS, + ), + ValveControllerButtonDescription( + key=BUTTON_KIND_RESET_VALVE_DIAGNOSTICS, + name="Reset valve diagnostics", + push_action=_async_valve_reset, + # Buttons don't actually need a coordinator; we give them one so they can + # properly inherit from GuardianEntity: + api_category=API_SYSTEM_DIAGNOSTICS, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Guardian buttons based on a config entry.""" + data: GuardianData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + GuardianButton(entry, data, description) for description in BUTTON_DESCRIPTIONS + ) + + +class GuardianButton(ValveControllerEntity, ButtonEntity): + """Define a Guardian button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG + + entity_description: ValveControllerButtonDescription + + def __init__( + self, + entry: ConfigEntry, + data: GuardianData, + description: ValveControllerButtonDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, data.valve_controller_coordinators, description) + + self._client = data.client + + async def async_press(self) -> None: + """Send out a restart command.""" + try: + async with self._client: + await self.entity_description.push_action(self._client) + except GuardianError as err: + raise HomeAssistantError( + f'Error while pressing button "{self.entity_id}": {err}' + ) from err + + async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested) diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index 3499db24c03..c7d025ba712 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -14,8 +14,4 @@ API_WIFI_STATUS = "wifi_status" CONF_UID = "uid" -DATA_CLIENT = "client" -DATA_COORDINATOR = "coordinator" -DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor" - SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED = "guardian_paired_sensor_coordinator_added_{0}" diff --git a/homeassistant/components/guardian/diagnostics.py b/homeassistant/components/guardian/diagnostics.py index 175136b33f4..d53dcb68fa8 100644 --- a/homeassistant/components/guardian/diagnostics.py +++ b/homeassistant/components/guardian/diagnostics.py @@ -7,8 +7,8 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import CONF_UID, DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN -from .util import GuardianDataUpdateCoordinator +from . import GuardianData +from .const import CONF_UID, DOMAIN CONF_BSSID = "bssid" CONF_PAIRED_UIDS = "paired_uids" @@ -26,12 +26,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = hass.data[DOMAIN][entry.entry_id] - - coordinators: dict[str, GuardianDataUpdateCoordinator] = data[DATA_COORDINATOR] - paired_sensor_coordinators: dict[str, GuardianDataUpdateCoordinator] = data[ - DATA_COORDINATOR_PAIRED_SENSOR - ] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] return { "entry": { @@ -41,11 +36,11 @@ async def async_get_config_entry_diagnostics( "data": { "valve_controller": { api_category: async_redact_data(coordinator.data, TO_REDACT) - for api_category, coordinator in coordinators.items() + for api_category, coordinator in data.valve_controller_coordinators.items() }, "paired_sensors": [ async_redact_data(coordinator.data, TO_REDACT) - for coordinator in paired_sensor_coordinators.values() + for coordinator in data.paired_sensor_manager.coordinators.values() ], }, } diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index fe9a453a166..7fab487563c 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,7 +3,7 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "requirements": ["aioguardian==2022.03.2"], + "requirements": ["aioguardian==2022.07.0"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], "iot_class": "local_polling", diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 895452e0fda..05de437b10a 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -1,6 +1,8 @@ """Sensors for the Elexa Guardian integration.""" from __future__ import annotations +from dataclasses import dataclass + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -8,19 +10,22 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_FAHRENHEIT, TIME_MINUTES +from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, TEMP_FAHRENHEIT, TIME_MINUTES from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import PairedSensorEntity, ValveControllerEntity +from . import ( + GuardianData, + PairedSensorEntity, + ValveControllerEntity, + ValveControllerEntityDescription, +) from .const import ( API_SYSTEM_DIAGNOSTICS, API_SYSTEM_ONBOARD_SENSOR_STATUS, CONF_UID, - DATA_COORDINATOR, - DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) @@ -29,35 +34,47 @@ SENSOR_KIND_BATTERY = "battery" SENSOR_KIND_TEMPERATURE = "temperature" SENSOR_KIND_UPTIME = "uptime" -SENSOR_DESCRIPTION_BATTERY = SensorEntityDescription( - key=SENSOR_KIND_BATTERY, - name="Battery", - device_class=SensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, -) -SENSOR_DESCRIPTION_TEMPERATURE = SensorEntityDescription( - key=SENSOR_KIND_TEMPERATURE, - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_FAHRENHEIT, - state_class=SensorStateClass.MEASUREMENT, -) -SENSOR_DESCRIPTION_UPTIME = SensorEntityDescription( - key=SENSOR_KIND_UPTIME, - name="Uptime", - icon="mdi:timer", - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=TIME_MINUTES, -) + +@dataclass +class ValveControllerSensorDescription( + SensorEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller sensor.""" + PAIRED_SENSOR_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_BATTERY, - SENSOR_DESCRIPTION_TEMPERATURE, + SensorEntityDescription( + key=SENSOR_KIND_BATTERY, + name="Battery", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + ), + SensorEntityDescription( + key=SENSOR_KIND_TEMPERATURE, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + ), ) VALVE_CONTROLLER_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_TEMPERATURE, - SENSOR_DESCRIPTION_UPTIME, + ValveControllerSensorDescription( + key=SENSOR_KIND_TEMPERATURE, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + api_category=API_SYSTEM_ONBOARD_SENSOR_STATUS, + ), + ValveControllerSensorDescription( + key=SENSOR_KIND_UPTIME, + name="Uptime", + icon="mdi:timer", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=TIME_MINUTES, + api_category=API_SYSTEM_DIAGNOSTICS, + ), ) @@ -65,19 +82,16 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" + data: GuardianData = hass.data[DOMAIN][entry.entry_id] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ - uid - ] - async_add_entities( - [ - PairedSensorSensor(entry, coordinator, description) - for description in PAIRED_SENSOR_DESCRIPTIONS - ] + PairedSensorSensor( + entry, data.paired_sensor_manager.coordinators[uid], description + ) + for description in PAIRED_SENSOR_DESCRIPTIONS ) # Handle adding paired sensors after HASS startup: @@ -91,9 +105,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorSensor | ValveControllerSensor] = [ - ValveControllerSensor( - entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description - ) + ValveControllerSensor(entry, data.valve_controller_coordinators, description) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -101,9 +113,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorSensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ].values() + for coordinator in data.paired_sensor_manager.coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) @@ -114,9 +124,11 @@ async def async_setup_entry( class PairedSensorSensor(PairedSensorEntity, SensorEntity): """Define a binary sensor related to a Guardian valve controller.""" + entity_description: SensorEntityDescription + @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_BATTERY: self._attr_native_value = self.coordinator.data["battery"] elif self.entity_description.key == SENSOR_KIND_TEMPERATURE: @@ -126,25 +138,12 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): class ValveControllerSensor(ValveControllerEntity, SensorEntity): """Define a generic Guardian sensor.""" - async def _async_continue_entity_setup(self) -> None: - """Register API interest (and related tasks) when the entity is added.""" - if self.entity_description.key == SENSOR_KIND_TEMPERATURE: - self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS) + entity_description: ValveControllerSensorDescription @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_TEMPERATURE: - self._attr_available = self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].last_update_success - self._attr_native_value = self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].data["temperature"] + self._attr_native_value = self.coordinator.data["temperature"] elif self.entity_description.key == SENSOR_KIND_UPTIME: - self._attr_available = self.coordinators[ - API_SYSTEM_DIAGNOSTICS - ].last_update_success - self._attr_native_value = self.coordinators[API_SYSTEM_DIAGNOSTICS].data[ - "uptime" - ] + self._attr_native_value = self.coordinator.data["uptime"] diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index 4d48783c955..61cf709a31c 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -39,28 +39,6 @@ pair_sensor: example: 5410EC688BCF selector: text: -reboot: - name: Reboot - description: Reboot the device. - fields: - device_id: - name: Valve Controller - description: The valve controller to reboot - required: true - selector: - device: - integration: guardian -reset_valve_diagnostics: - name: Reset Valve Diagnostics - description: Fully (and irrecoverably) reset all valve diagnostics. - fields: - device_id: - name: Valve Controller - description: The valve controller whose diagnostics should be reset - required: true - selector: - device: - integration: guardian unpair_sensor: name: Unpair Sensor description: Remove a paired sensor from the valve controller. diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 9a4f70fd3d2..4e100ce4fe4 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,9 +1,9 @@ """Switches for the Elexa Guardian integration.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any -from aioguardian import Client from aioguardian.errors import GuardianError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -11,10 +11,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import ValveControllerEntity -from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN +from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription +from .const import API_VALVE_STATUS, DOMAIN ATTR_AVG_CURRENT = "average_current" ATTR_INST_CURRENT = "instantaneous_current" @@ -23,10 +22,21 @@ ATTR_TRAVEL_COUNT = "travel_count" SWITCH_KIND_VALVE = "valve" -SWITCH_DESCRIPTION_VALVE = SwitchEntityDescription( - key=SWITCH_KIND_VALVE, - name="Valve Controller", - icon="mdi:water", + +@dataclass +class ValveControllerSwitchDescription( + SwitchEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller switch.""" + + +VALVE_CONTROLLER_DESCRIPTIONS = ( + ValveControllerSwitchDescription( + key=SWITCH_KIND_VALVE, + name="Valve controller", + icon="mdi:water", + api_category=API_VALVE_STATUS, + ), ) @@ -34,61 +44,50 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" + data: GuardianData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( - [ - ValveControllerSwitch( - entry, - hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], - ) - ] + ValveControllerSwitch(entry, data, description) + for description in VALVE_CONTROLLER_DESCRIPTIONS ) class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): """Define a switch to open/close the Guardian valve.""" + entity_description: ValveControllerSwitchDescription + + ON_STATES = { + "start_opening", + "opening", + "finish_opening", + "opened", + } + def __init__( self, entry: ConfigEntry, - client: Client, - coordinators: dict[str, DataUpdateCoordinator], + data: GuardianData, + description: ValveControllerSwitchDescription, ) -> None: """Initialize.""" - super().__init__(entry, coordinators, SWITCH_DESCRIPTION_VALVE) + super().__init__(entry, data.valve_controller_coordinators, description) self._attr_is_on = True - self._client = client - - async def _async_continue_entity_setup(self) -> None: - """Register API interest (and related tasks) when the entity is added.""" - self.async_add_coordinator_update_listener(API_VALVE_STATUS) + self._client = data.client @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" - self._attr_available = self.coordinators[API_VALVE_STATUS].last_update_success - self._attr_is_on = self.coordinators[API_VALVE_STATUS].data["state"] in ( - "start_opening", - "opening", - "finish_opening", - "opened", - ) - + self._attr_is_on = self.coordinator.data["state"] in self.ON_STATES self._attr_extra_state_attributes.update( { - ATTR_AVG_CURRENT: self.coordinators[API_VALVE_STATUS].data[ - "average_current" - ], - ATTR_INST_CURRENT: self.coordinators[API_VALVE_STATUS].data[ - "instantaneous_current" - ], - ATTR_INST_CURRENT_DDT: self.coordinators[API_VALVE_STATUS].data[ + ATTR_AVG_CURRENT: self.coordinator.data["average_current"], + ATTR_INST_CURRENT: self.coordinator.data["instantaneous_current"], + ATTR_INST_CURRENT_DDT: self.coordinator.data[ "instantaneous_current_ddt" ], - ATTR_TRAVEL_COUNT: self.coordinators[API_VALVE_STATUS].data[ - "travel_count" - ], + ATTR_TRAVEL_COUNT: self.coordinator.data["travel_count"], } ) diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 2cedcf9c1e4..c88d6762e51 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -9,21 +9,28 @@ from typing import Any, cast from aioguardian import Client from aioguardian.errors import GuardianError -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) +SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}" + class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): """Define an extended DataUpdateCoordinator with some Guardian goodies.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, *, + entry: ConfigEntry, client: Client, api_name: str, api_coro: Callable[..., Awaitable], @@ -41,6 +48,12 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): self._api_coro = api_coro self._api_lock = api_lock self._client = client + self._signal_handler_unsubs: list[Callable[..., None]] = [] + + self.config_entry = entry + self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( + self.config_entry.entry_id + ) async def _async_update_data(self) -> dict[str, Any]: """Execute a "locked" API request against the valve controller.""" @@ -50,3 +63,26 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): except GuardianError as err: raise UpdateFailed(err) from err return cast(dict[str, Any], resp["data"]) + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + + @callback + def async_reboot_requested() -> None: + """Respond to a reboot request.""" + self.last_update_success = False + self.async_update_listeners() + + self._signal_handler_unsubs.append( + async_dispatcher_connect( + self.hass, self.signal_reboot_requested, async_reboot_requested + ) + ) + + @callback + def async_teardown() -> None: + """Tear the coordinator down appropriately.""" + for unsub in self._signal_handler_unsubs: + unsub() + + self.config_entry.async_on_unload(async_teardown) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 3cadd6897d2..25738893689 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -155,7 +155,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) data[entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): hass.services.async_register( diff --git a/homeassistant/components/hangouts/translations/pt.json b/homeassistant/components/hangouts/translations/pt.json index 093deaecc15..b4feb91c76d 100644 --- a/homeassistant/components/hangouts/translations/pt.json +++ b/homeassistant/components/hangouts/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts j\u00e1 est\u00e1 configurado", - "unknown": "Ocorreu um erro desconhecido." + "unknown": "Erro inesperado" }, "error": { "invalid_2fa": "Autentica\u00e7\u00e3o por 2 fatores inv\u00e1lida, por favor, tente novamente.", diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 4e109ae95a7..259ea660317 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CANCEL_STOP: cancel_stop, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 16101f18cff..675acf600cb 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import logging +from typing import Any from urllib.parse import urlparse from aioharmony.hubconnector_websocket import HubConnector @@ -57,7 +58,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Harmony config flow.""" - self.harmony_config = {} + self.harmony_config: dict[str, Any] = {} async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index aa373d5813a..fbbbbd38e3a 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -21,13 +21,14 @@ _LOGGER = logging.getLogger(__name__) class HarmonyData(HarmonySubscriberMixin): """HarmonyData registers for Harmony hub updates.""" - def __init__(self, hass, address: str, name: str, unique_id: str): + _client: HarmonyClient + + def __init__(self, hass, address: str, name: str, unique_id: str | None) -> None: """Initialize a data object.""" super().__init__(hass) self._name = name self._unique_id = unique_id self._available = False - self._client = None self._address = address @property @@ -99,7 +100,7 @@ class HarmonyData(HarmonySubscriberMixin): configuration_url="https://www.logitech.com/en-us/my-account", ) - async def connect(self) -> bool: + async def connect(self) -> None: """Connect to the Harmony Hub.""" _LOGGER.debug("%s: Connecting", self._name) diff --git a/homeassistant/components/harmony/translations/pt.json b/homeassistant/components/harmony/translations/pt.json index 04374af8e82..3b58778917d 100644 --- a/homeassistant/components/harmony/translations/pt.json +++ b/homeassistant/components/harmony/translations/pt.json @@ -14,5 +14,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "A actividade por defeito a executar quando nenhuma \u00e9 especificada." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index cd3c704d4c9..46592cbc20c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -504,7 +504,7 @@ def is_hassio(hass: HomeAssistant) -> bool: @callback -def get_supervisor_ip() -> str: +def get_supervisor_ip() -> str | None: """Return the supervisor ip address.""" if "SUPERVISOR" not in os.environ: return None @@ -533,7 +533,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if not await hassio.is_connected(): _LOGGER.warning("Not connected with the supervisor / system too busy!") - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, str]](hass, STORAGE_VERSION, STORAGE_KEY) if (data := await store.async_load()) is None: data = {} @@ -710,6 +710,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async_setup_discovery_view(hass, hassio) # Init auth Hass.io feature + assert user is not None async_setup_auth_view(hass, user) # Init ingress Hass.io feature @@ -755,7 +756,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[ADDONS_COORDINATOR] = coordinator await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -877,7 +878,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): except HassioAPIError as err: raise UpdateFailed(f"Error on Supervisor API: {err}") from err - new_data = {} + new_data: dict[str, Any] = {} supervisor_info = get_supervisor_info(self.hass) addons_info = get_addons_info(self.hass) addons_stats = get_addons_stats(self.hass) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index f52a8ef0617..37687ee70df 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -43,6 +43,7 @@ class HassIOBaseAuth(HomeAssistantView): """Check if this call is from Supervisor.""" # Check caller IP hassio_ip = os.environ["SUPERVISOR"].split(":")[0] + assert request.transport if ip_address(request.transport.get_extra_info("peername")[0]) != ip_address( hassio_ip ): diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index c2bcd5eaf68..6ddc15e7725 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -36,7 +36,7 @@ COMMON_ENTITY_DESCRIPTIONS = ( device_class=BinarySensorDeviceClass.UPDATE, entity_registry_enabled_default=False, key=ATTR_UPDATE_AVAILABLE, - name="Update Available", + name="Update available", ), ) @@ -59,7 +59,7 @@ async def async_setup_entry( """Binary sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities = [] + entities: list[HassioAddonBinarySensor | HassioOSBinarySensor] = [] for entity_description in ADDON_ENTITY_DESCRIPTIONS: for addon in coordinator.data[DATA_KEY_ADDONS].values(): diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index c6bec04123c..dfa89ae911a 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any -from homeassistant.const import ATTR_NAME from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -20,6 +19,8 @@ from .const import ( class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base entity for a Hass.io add-on.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -30,7 +31,6 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): super().__init__(coordinator) self.entity_description = entity_description self._addon_slug = addon[ATTR_SLUG] - self._attr_name = f"{addon[ATTR_NAME]}: {entity_description.name}" self._attr_unique_id = f"{addon[ATTR_SLUG]}_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, addon[ATTR_SLUG])}) @@ -48,6 +48,8 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Hass.io OS.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -56,7 +58,6 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Initialize base entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = f"Home Assistant Operating System: {entity_description.name}" self._attr_unique_id = f"home_assistant_os_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "OS")}) @@ -73,6 +74,8 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Supervisor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -81,7 +84,6 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Initialize base entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = f"Home Assistant Supervisor: {entity_description.name}" self._attr_unique_id = f"home_assistant_supervisor_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "supervisor")}) @@ -99,6 +101,8 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Core.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -107,7 +111,6 @@ class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Initialize base entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = f"Home Assistant Core: {entity_description.name}" self._attr_unique_id = f"home_assistant_core_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "core")}) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 6caa97b788f..8aacbac99f6 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from ipaddress import ip_address import logging import os @@ -73,6 +74,7 @@ class HassIOIngress(HomeAssistantView): self, request: web.Request, token: str, path: str ) -> web.WebSocketResponse: """Ingress route for websocket.""" + req_protocols: Iterable[str] if hdrs.SEC_WEBSOCKET_PROTOCOL in request.headers: req_protocols = [ str(proto.strip()) @@ -190,6 +192,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st # Set X-Forwarded-For forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) + assert request.transport if (peername := request.transport.get_extra_info("peername")) is None: _LOGGER.error("Can't set forward_for header, missing peername") raise HTTPBadRequest() diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 42be1ff4b0a..31e728a9736 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -31,7 +31,7 @@ COMMON_ENTITY_DESCRIPTIONS = ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_VERSION_LATEST, - name="Newest Version", + name="Newest version", ), ) @@ -39,7 +39,7 @@ ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_CPU_PERCENT, - name="CPU Percent", + name="CPU percent", icon="mdi:cpu-64-bit", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -47,7 +47,7 @@ ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_MEMORY_PERCENT, - name="Memory Percent", + name="Memory percent", icon="mdi:memory", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -65,7 +65,7 @@ async def async_setup_entry( """Sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities = [] + entities: list[HassioOSSensor | HassioAddonSensor] = [] for addon in coordinator.data[DATA_KEY_ADDONS].values(): for entity_description in ADDON_ENTITY_DESCRIPTIONS: diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index b1fc208de80..d8d29f44d68 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -1,4 +1,6 @@ """Provide info to system health.""" +from __future__ import annotations + import os from homeassistant.components import system_health @@ -24,6 +26,7 @@ async def system_health_info(hass: HomeAssistant): host_info = get_host_info(hass) supervisor_info = get_supervisor_info(hass) + healthy: bool | dict[str, str] if supervisor_info.get("healthy"): healthy = True else: @@ -32,6 +35,7 @@ async def system_health_info(hass: HomeAssistant): "error": "Unhealthy", } + supported: bool | dict[str, str] if supervisor_info.get("supported"): supported = True else: diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index f9572edcd6f..5e1caa41ebf 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u0430\u0433\u0435\u043d\u0442\u0430", "board": "\u041f\u043b\u0430\u0442\u0430", "disk_total": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", "disk_used": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u043e", diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 7eb037d8432..eb0d6c54077 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -1,5 +1,6 @@ """Websocekt API handlers for the hassio integration.""" import logging +from numbers import Number import re import voluptuous as vol @@ -56,8 +57,8 @@ def async_load_websocket_api(hass: HomeAssistant): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(WS_TYPE): WS_TYPE_SUBSCRIBE}) +@websocket_api.async_response async def websocket_subscribe( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): @@ -74,31 +75,31 @@ async def websocket_subscribe( connection.send_message(websocket_api.result_message(msg[WS_ID])) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(WS_TYPE): WS_TYPE_EVENT, vol.Required(ATTR_DATA): SCHEMA_WEBSOCKET_EVENT, } ) +@websocket_api.async_response async def websocket_supervisor_event( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): """Publish events from the Supervisor.""" - async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA]) connection.send_result(msg[WS_ID]) + async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA]) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(WS_TYPE): WS_TYPE_API, vol.Required(ATTR_ENDPOINT): cv.string, vol.Required(ATTR_METHOD): cv.string, vol.Optional(ATTR_DATA): dict, - vol.Optional(ATTR_TIMEOUT): vol.Any(cv.Number, None), + vol.Optional(ATTR_TIMEOUT): vol.Any(Number, None), } ) +@websocket_api.async_response async def websocket_supervisor_api( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json index 8996c2a4530..49bee7bac8f 100644 --- a/homeassistant/components/heos/translations/hu.json +++ b/homeassistant/components/heos/translations/hu.json @@ -11,7 +11,7 @@ "data": { "host": "C\u00edm" }, - "description": "K\u00e9rj\u00fck, adja meg egy Heos-eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t (lehet\u0151leg egy vezet\u00e9kkel a h\u00e1l\u00f3zathoz csatlakoztatott eszk\u00f6zt).", + "description": "K\u00e9rem, adja meg egy Heos-eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t (lehet\u0151leg egy vezet\u00e9kes h\u00e1l\u00f3zathoz csatlakoztatott eszk\u00f6zt).", "title": "Csatlakoz\u00e1s a Heos-hoz" } } diff --git a/homeassistant/components/heos/translations/ja.json b/homeassistant/components/heos/translations/ja.json index 55e075a548a..9a6e9513ecd 100644 --- a/homeassistant/components/heos/translations/ja.json +++ b/homeassistant/components/heos/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 2b9853c3b10..549cc08356c 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b here_travel_time_config, ) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True @@ -187,8 +187,8 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): return HERERoutingData( { ATTR_ATTRIBUTION: attribution, - ATTR_DURATION: summary["baseTime"] / 60, # type: ignore[misc] - ATTR_DURATION_IN_TRAFFIC: traffic_time / 60, + ATTR_DURATION: round(summary["baseTime"] / 60), # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: round(traffic_time / 60), ATTR_DISTANCE: distance, ATTR_ROUTE: response.route_short, ATTR_ORIGIN: ",".join(origin), diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index bde17f5c306..b3768b2d69d 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -24,8 +24,6 @@ CONF_DEPARTURE_TIME = "departure_time" DEFAULT_NAME = "HERE Travel Time" -TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] - TRAVEL_MODE_BICYCLE = "bicycle" TRAVEL_MODE_CAR = "car" TRAVEL_MODE_PEDESTRIAN = "pedestrian" @@ -41,7 +39,6 @@ TRAVEL_MODES = [ TRAVEL_MODE_TRUCK, ] -TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] TRAFFIC_MODE_ENABLED = "traffic_enabled" @@ -58,6 +55,14 @@ ICON_PEDESTRIAN = "mdi:walk" ICON_PUBLIC = "mdi:bus" ICON_TRUCK = "mdi:truck" +ICONS = { + TRAVEL_MODE_BICYCLE: ICON_BICYCLE, + TRAVEL_MODE_PEDESTRIAN: ICON_PEDESTRIAN, + TRAVEL_MODE_PUBLIC: ICON_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE: ICON_PUBLIC, + TRAVEL_MODE_TRUCK: ICON_TRUCK, +} + UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] ATTR_DURATION = "duration" diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 75c9fd2ea3b..c4be60d5569 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,16 +1,24 @@ """Support for HERE travel time sensors.""" from __future__ import annotations +from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_MODE, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_API_KEY, CONF_MODE, CONF_NAME, @@ -28,10 +36,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import HereTravelTimeDataUpdateCoordinator from .const import ( + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, ATTR_DURATION, ATTR_DURATION_IN_TRAFFIC, - ATTR_TRAFFIC_MODE, - ATTR_UNIT_SYSTEM, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + ATTR_ROUTE, CONF_ARRIVAL, CONF_DEPARTURE, CONF_DESTINATION_ENTITY_ID, @@ -44,14 +56,10 @@ from .const import ( CONF_TRAFFIC_MODE, DEFAULT_NAME, DOMAIN, - ICON_BICYCLE, ICON_CAR, - ICON_PEDESTRIAN, - ICON_PUBLIC, - ICON_TRUCK, + ICONS, ROUTE_MODE_FASTEST, ROUTE_MODES, - TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, @@ -59,7 +67,6 @@ from .const import ( TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, TRAVEL_MODES, - TRAVEL_MODES_PUBLIC, UNITS, ) @@ -115,6 +122,69 @@ PLATFORM_SCHEMA = vol.All( ) +def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]: + """Construct SensorEntityDescriptions.""" + return ( + SensorEntityDescription( + name="Duration", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + name="Duration in Traffic", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DURATION_IN_TRAFFIC, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + name="Distance", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + name="Route", + icon="mdi:directions", + key=ATTR_ROUTE, + ), + ) + + +def create_origin_sensor( + config_entry: ConfigEntry, hass: HomeAssistant +) -> OriginSensor: + """Create a origin sensor.""" + return OriginSensor( + config_entry.entry_id, + config_entry.data[CONF_NAME], + SensorEntityDescription( + name="Origin", + icon="mdi:store-marker", + key=ATTR_ORIGIN_NAME, + ), + hass.data[DOMAIN][config_entry.entry_id], + ) + + +def create_destination_sensor( + config_entry: ConfigEntry, hass: HomeAssistant +) -> DestinationSensor: + """Create a destination sensor.""" + return DestinationSensor( + config_entry.entry_id, + config_entry.data[CONF_NAME], + SensorEntityDescription( + name="Destination", + icon="mdi:store-marker", + key=ATTR_DESTINATION_NAME, + ), + hass.data[DOMAIN][config_entry.entry_id], + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -143,16 +213,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add HERE travel time entities from a config_entry.""" - async_add_entities( - [ + + sensors: list[HERETravelTimeSensor] = [] + for sensor_description in sensor_descriptions(config_entry.data[CONF_MODE]): + sensors.append( HERETravelTimeSensor( config_entry.entry_id, config_entry.data[CONF_NAME], - config_entry.options[CONF_TRAFFIC_MODE], + sensor_description, hass.data[DOMAIN][config_entry.entry_id], ) - ], - ) + ) + sensors.append(create_origin_sensor(config_entry, hass)) + sensors.append(create_destination_sensor(config_entry, hass)) + async_add_entities(sensors) class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): @@ -162,15 +236,14 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): self, unique_id_prefix: str, name: str, - traffic_mode: str, + sensor_description: SensorEntityDescription, coordinator: HereTravelTimeDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._traffic_mode = traffic_mode == TRAFFIC_MODE_ENABLED - self._attr_native_unit_of_measurement = TIME_MINUTES - self._attr_name = name - self._attr_unique_id = unique_id_prefix + self.entity_description = sensor_description + self._attr_name = f"{name} {sensor_description.name}" + self._attr_unique_id = f"{unique_id_prefix}_{sensor_description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id_prefix)}, entry_type=DeviceEntryType.SERVICE, @@ -188,34 +261,10 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): self.async_on_remove(async_at_start(self.hass, _update_at_start)) @property - def native_value(self) -> str | None: + def native_value(self) -> str | float | None: """Return the state of the sensor.""" if self.coordinator.data is not None: - return str( - round( - self.coordinator.data.get( - ATTR_DURATION_IN_TRAFFIC - if self._traffic_mode - else ATTR_DURATION - ) - ) - ) - return None - - @property - def extra_state_attributes( - self, - ) -> dict[str, None | float | str | bool] | None: - """Return the state attributes.""" - if self.coordinator.data is not None: - res = { - ATTR_UNIT_SYSTEM: self.coordinator.config.units, - ATTR_MODE: self.coordinator.config.travel_mode, - ATTR_TRAFFIC_MODE: self._traffic_mode, - **self.coordinator.data, - } - res.pop(ATTR_ATTRIBUTION) - return res + return self.coordinator.data.get(self.entity_description.key) return None @property @@ -225,15 +274,30 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): return self.coordinator.data.get(ATTR_ATTRIBUTION) return None + +class OriginSensor(HERETravelTimeSensor): + """Sensor holding information about the route origin.""" + @property - def icon(self) -> str: - """Icon to use in the frontend depending on travel_mode.""" - if self.coordinator.config.travel_mode == TRAVEL_MODE_BICYCLE: - return ICON_BICYCLE - if self.coordinator.config.travel_mode == TRAVEL_MODE_PEDESTRIAN: - return ICON_PEDESTRIAN - if self.coordinator.config.travel_mode in TRAVEL_MODES_PUBLIC: - return ICON_PUBLIC - if self.coordinator.config.travel_mode == TRAVEL_MODE_TRUCK: - return ICON_TRUCK - return ICON_CAR + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """GPS coordinates.""" + if self.coordinator.data is not None: + return { + ATTR_LATITUDE: self.coordinator.data[ATTR_ORIGIN].split(",")[0], + ATTR_LONGITUDE: self.coordinator.data[ATTR_ORIGIN].split(",")[1], + } + return None + + +class DestinationSensor(HERETravelTimeSensor): + """Sensor holding information about the route destination.""" + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """GPS coordinates.""" + if self.coordinator.data is not None: + return { + ATTR_LATITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[0], + ATTR_LONGITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[1], + } + return None diff --git a/homeassistant/components/here_travel_time/translations/ar.json b/homeassistant/components/here_travel_time/translations/ar.json new file mode 100644 index 00000000000..adea555a76a --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/ar.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0645\u0648\u0642\u0639 \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "origin_entity": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0643\u064a\u0627\u0646" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/ca.json b/homeassistant/components/here_travel_time/translations/ca.json index 08dfb0d970d..586e5bd5442 100644 --- a/homeassistant/components/here_travel_time/translations/ca.json +++ b/homeassistant/components/here_travel_time/translations/ca.json @@ -39,6 +39,13 @@ }, "title": "Tria origen" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Utilitzant una ubicaci\u00f3 de mapa", + "origin_entity": "Utilitzant una entitat" + }, + "title": "Tria l'origen" + }, "user": { "data": { "api_key": "Clau API", diff --git a/homeassistant/components/here_travel_time/translations/de.json b/homeassistant/components/here_travel_time/translations/de.json index b50ef028089..db93b79fd2f 100644 --- a/homeassistant/components/here_travel_time/translations/de.json +++ b/homeassistant/components/here_travel_time/translations/de.json @@ -23,7 +23,7 @@ "destination_menu": { "menu_options": { "destination_coordinates": "Verwendung einer Kartenposition", - "destination_entity": "Verwenden einer Entit\u00e4t" + "destination_entity": "Verwendung einer Entit\u00e4t" }, "title": "Ziel w\u00e4hlen" }, @@ -39,6 +39,13 @@ }, "title": "Herkunft w\u00e4hlen" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Verwendung einer Kartenposition", + "origin_entity": "Verwendung einer Entit\u00e4t" + }, + "title": "Herkunft w\u00e4hlen" + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", diff --git a/homeassistant/components/here_travel_time/translations/el.json b/homeassistant/components/here_travel_time/translations/el.json index cd4d986ed3f..062cf89ea0f 100644 --- a/homeassistant/components/here_travel_time/translations/el.json +++ b/homeassistant/components/here_travel_time/translations/el.json @@ -39,6 +39,13 @@ }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b8\u03ad\u03c3\u03b7\u03c2 \u03c7\u03ac\u03c1\u03c4\u03b7", + "origin_entity": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2" + }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", diff --git a/homeassistant/components/here_travel_time/translations/et.json b/homeassistant/components/here_travel_time/translations/et.json index c9646af7544..a017971a33f 100644 --- a/homeassistant/components/here_travel_time/translations/et.json +++ b/homeassistant/components/here_travel_time/translations/et.json @@ -39,6 +39,13 @@ }, "title": "Vali l\u00e4htekoht" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Kasuta asukohta kaardil", + "origin_entity": "Kasuta olemi andmeid" + }, + "title": "Vali l\u00e4htepunkt" + }, "user": { "data": { "api_key": "API v\u00f5ti", diff --git a/homeassistant/components/here_travel_time/translations/fr.json b/homeassistant/components/here_travel_time/translations/fr.json index 063a4008779..c44c2337af9 100644 --- a/homeassistant/components/here_travel_time/translations/fr.json +++ b/homeassistant/components/here_travel_time/translations/fr.json @@ -39,6 +39,13 @@ }, "title": "Choix de l'origine" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Utilisation d'un emplacement sur la carte", + "origin_entity": "Utilisation d'une entit\u00e9" + }, + "title": "Choix de l'origine" + }, "user": { "data": { "api_key": "Cl\u00e9 d'API", diff --git a/homeassistant/components/here_travel_time/translations/hu.json b/homeassistant/components/here_travel_time/translations/hu.json index cdd40f139d7..b3cee83f662 100644 --- a/homeassistant/components/here_travel_time/translations/hu.json +++ b/homeassistant/components/here_travel_time/translations/hu.json @@ -39,6 +39,13 @@ }, "title": "Eredet kiv\u00e1laszt\u00e1sa" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "T\u00e9rk\u00e9pes hely haszn\u00e1lata", + "origin_entity": "Egy entit\u00e1s haszn\u00e1lata" + }, + "title": "V\u00e1lassza az eredetit" + }, "user": { "data": { "api_key": "API kulcs", diff --git a/homeassistant/components/here_travel_time/translations/id.json b/homeassistant/components/here_travel_time/translations/id.json index f03910d9adf..57d4ec5ee80 100644 --- a/homeassistant/components/here_travel_time/translations/id.json +++ b/homeassistant/components/here_travel_time/translations/id.json @@ -39,6 +39,13 @@ }, "title": "Pilih Asal" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Menggunakan lokasi pada peta", + "origin_entity": "Menggunakan entitas" + }, + "title": "Pilih Asal" + }, "user": { "data": { "api_key": "Kunci API", diff --git a/homeassistant/components/here_travel_time/translations/it.json b/homeassistant/components/here_travel_time/translations/it.json index e9716318adb..17cd09fed8b 100644 --- a/homeassistant/components/here_travel_time/translations/it.json +++ b/homeassistant/components/here_travel_time/translations/it.json @@ -22,8 +22,8 @@ }, "destination_menu": { "menu_options": { - "destination_coordinates": "Utilizzando una posizione sulla mappa", - "destination_entity": "Utilizzando un'entit\u00e0" + "destination_coordinates": "Utilizzo di una posizione sulla mappa", + "destination_entity": "Utilizzo di un'entit\u00e0" }, "title": "Scegli la destinazione" }, @@ -39,6 +39,13 @@ }, "title": "Scegli la partenza" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Utilizzo di una posizione sulla mappa", + "origin_entity": "Utilizzo di un'entit\u00e0" + }, + "title": "Scegli la partenza" + }, "user": { "data": { "api_key": "Chiave API", diff --git a/homeassistant/components/here_travel_time/translations/ja.json b/homeassistant/components/here_travel_time/translations/ja.json index 6db262d829a..ae3eb1d4f73 100644 --- a/homeassistant/components/here_travel_time/translations/ja.json +++ b/homeassistant/components/here_travel_time/translations/ja.json @@ -39,6 +39,13 @@ }, "title": "\u539f\u70b9(Origin)\u3092\u9078\u629e" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u5730\u56f3\u4e0a\u306e\u5834\u6240\u3092\u4f7f\u7528\u3059\u308b", + "origin_entity": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4f7f\u7528\u3059\u308b" + }, + "title": "\u539f\u70b9(Origin)\u3092\u9078\u629e" + }, "user": { "data": { "api_key": "API\u30ad\u30fc", diff --git a/homeassistant/components/here_travel_time/translations/nl.json b/homeassistant/components/here_travel_time/translations/nl.json index cbec21776e5..7d3438901cc 100644 --- a/homeassistant/components/here_travel_time/translations/nl.json +++ b/homeassistant/components/here_travel_time/translations/nl.json @@ -39,6 +39,12 @@ }, "title": "Herkomst kiezen" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Een kaartlocatie gebruiken", + "origin_entity": "Een entiteit gebruiken" + } + }, "user": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/here_travel_time/translations/no.json b/homeassistant/components/here_travel_time/translations/no.json index 52d4477f379..e4282933051 100644 --- a/homeassistant/components/here_travel_time/translations/no.json +++ b/homeassistant/components/here_travel_time/translations/no.json @@ -39,6 +39,11 @@ }, "title": "Velg Opprinnelse" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Bruk kartplassering" + } + }, "user": { "data": { "api_key": "API-n\u00f8kkel", diff --git a/homeassistant/components/here_travel_time/translations/pl.json b/homeassistant/components/here_travel_time/translations/pl.json index 3e4f41212a2..17b91417007 100644 --- a/homeassistant/components/here_travel_time/translations/pl.json +++ b/homeassistant/components/here_travel_time/translations/pl.json @@ -39,6 +39,13 @@ }, "title": "Wybierz punkt pocz\u0105tkowy" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Lokalizacja na mapie", + "origin_entity": "Encja" + }, + "title": "Wybierz punkt pocz\u0105tkowy" + }, "user": { "data": { "api_key": "Klucz API", diff --git a/homeassistant/components/here_travel_time/translations/pt-BR.json b/homeassistant/components/here_travel_time/translations/pt-BR.json index 34f862f0029..9b524df7539 100644 --- a/homeassistant/components/here_travel_time/translations/pt-BR.json +++ b/homeassistant/components/here_travel_time/translations/pt-BR.json @@ -39,6 +39,13 @@ }, "title": "Escolha a Origem" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Usando uma localiza\u00e7\u00e3o no mapa", + "origin_entity": "Usando uma entidade" + }, + "title": "Escolha a Origem" + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/here_travel_time/translations/pt.json b/homeassistant/components/here_travel_time/translations/pt.json new file mode 100644 index 00000000000..a36091fc30c --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "origin_menu": { + "menu_options": { + "origin_coordinates": "Usando uma localiza\u00e7\u00e3o no mapa", + "origin_entity": "Usando uma entidade" + }, + "title": "Escolha a Origem" + } + } + }, + "options": { + "step": { + "departure_time": { + "data": { + "departure_time": "Hora de partida" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/ru.json b/homeassistant/components/here_travel_time/translations/ru.json index fc649d920df..a89608530d8 100644 --- a/homeassistant/components/here_travel_time/translations/ru.json +++ b/homeassistant/components/here_travel_time/translations/ru.json @@ -39,6 +39,13 @@ }, "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u0423\u043a\u0430\u0437\u0430\u0442\u044c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", + "origin_entity": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/here_travel_time/translations/tr.json b/homeassistant/components/here_travel_time/translations/tr.json index 181588ba54a..10be792e0ee 100644 --- a/homeassistant/components/here_travel_time/translations/tr.json +++ b/homeassistant/components/here_travel_time/translations/tr.json @@ -39,6 +39,13 @@ }, "title": "Kalk\u0131\u015f Se\u00e7in" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Bir harita konumu kullan\u0131n", + "origin_entity": "Bir varl\u0131\u011f\u0131 kullan\u0131n" + }, + "title": "Kalk\u0131\u015f Se\u00e7in" + }, "user": { "data": { "api_key": "API Anahtar\u0131", diff --git a/homeassistant/components/here_travel_time/translations/zh-Hant.json b/homeassistant/components/here_travel_time/translations/zh-Hant.json index 53d5eae18fd..4476294b661 100644 --- a/homeassistant/components/here_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/here_travel_time/translations/zh-Hant.json @@ -39,6 +39,13 @@ }, "title": "\u9078\u64c7\u51fa\u767c\u5730" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u4f7f\u7528\u5730\u5716\u5ea7\u6a19", + "origin_entity": "\u4f7f\u7528\u70ba\u5be6\u9ad4" + }, + "title": "\u9078\u64c7\u51fa\u767c\u5730" + }, "user": { "data": { "api_key": "API \u91d1\u9470", diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py index 74eb4371614..cc599aa31fc 100644 --- a/homeassistant/components/hisense_aehw4a1/__init__.py +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -75,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for Hisense AEH-W4A1.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hisense_aehw4a1/translations/ja.json b/homeassistant/components/hisense_aehw4a1/translations/ja.json index 75107c4c0fc..edace613685 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/ja.json +++ b/homeassistant/components/hisense_aehw4a1/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 52cf7f719e6..a1a784162e8 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -7,7 +7,7 @@ import logging from typing import Any, TypeVar from aiohttp.web_exceptions import HTTPException -from apyhiveapi import Hive +from apyhiveapi import Auth, Hive from apyhiveapi.helper.hive_exceptions import HiveReauthRequired from typing_extensions import Concatenate, ParamSpec import voluptuous as vol @@ -93,12 +93,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except HiveReauthRequired as err: raise ConfigEntryAuthFailed from err - for ha_type, hive_type in PLATFORM_LOOKUP.items(): - device_list = devices.get(hive_type) - if device_list: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, ha_type) - ) + await hass.config_entries.async_forward_entry_setups( + entry, + [ + ha_type + for ha_type, hive_type in PLATFORM_LOOKUP.items() + if devices.get(hive_type) + ], + ) return True @@ -112,6 +114,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove a config entry.""" + hive = Auth(entry.data["username"], entry.data["password"]) + await hive.forget_device( + entry.data["tokens"]["AuthenticationResult"]["AccessToken"], + entry.data["device_data"][1], + ) + + def refresh_system( func: Callable[Concatenate[_HiveEntityT, _P], Awaitable[Any]] ) -> Callable[Concatenate[_HiveEntityT, _P], Coroutine[Any, Any, None]]: diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index d985f1a83bb..809feb19d5e 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, POWER_WATT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity @@ -23,12 +24,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="Power", native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, ), ) diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 64a8276521a..f72f228c595 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -6,6 +6,7 @@ from datetime import timedelta from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity, refresh_system @@ -19,7 +20,10 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="activeplug", ), - SwitchEntityDescription(key="Heating_Heat_On_Demand"), + SwitchEntityDescription( + key="Heating_Heat_On_Demand", + entity_category=EntityCategory.CONFIG, + ), ) diff --git a/homeassistant/components/hive/translations/ar.json b/homeassistant/components/hive/translations/ar.json new file mode 100644 index 00000000000..fd198262e7e --- /dev/null +++ b/homeassistant/components/hive/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "configuration": { + "data": { + "device_name": "\u0627\u0633\u0645 \u0627\u0644\u062c\u0647\u0627\u0632" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index ed11bbd8b7e..981c65ba540 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -41,7 +41,7 @@ "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "Hive\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3068\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "description": "Hive\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "Hive\u30ed\u30b0\u30a4\u30f3" } } diff --git a/homeassistant/components/hive/translations/pl.json b/homeassistant/components/hive/translations/pl.json index 0c61fa74feb..b227e5b1e9a 100644 --- a/homeassistant/components/hive/translations/pl.json +++ b/homeassistant/components/hive/translations/pl.json @@ -20,6 +20,13 @@ "description": "Wprowad\u017a sw\u00f3j kod uwierzytelniaj\u0105cy Hive. \n\nWprowad\u017a kod 0000, aby poprosi\u0107 o kolejny kod.", "title": "Uwierzytelnianie dwusk\u0142adnikowe Hive" }, + "configuration": { + "data": { + "device_name": "Nazwa urz\u0105dzenia" + }, + "description": "Wprowad\u017a konfiguracj\u0119 Hive", + "title": "Konfiguracja Hive." + }, "reauth": { "data": { "password": "Has\u0142o", @@ -34,7 +41,7 @@ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a dane logowania i konfiguracj\u0119 Hive.", + "description": "Wprowad\u017a dane logowania Hive.", "title": "Login Hive" } } diff --git a/homeassistant/components/hive/translations/ru.json b/homeassistant/components/hive/translations/ru.json index 02736871d24..160f948b0a3 100644 --- a/homeassistant/components/hive/translations/ru.json +++ b/homeassistant/components/hive/translations/ru.json @@ -20,6 +20,13 @@ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 Hive \u0438\u043b\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 0000, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u0434\u0440\u0443\u0433\u043e\u0439 \u043a\u043e\u0434.", "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, + "configuration": { + "data": { + "device_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Hive.", + "title": "Hive" + }, "reauth": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/hive/translations/tr.json b/homeassistant/components/hive/translations/tr.json index afc0ee66f03..158424f996a 100644 --- a/homeassistant/components/hive/translations/tr.json +++ b/homeassistant/components/hive/translations/tr.json @@ -20,6 +20,13 @@ "description": "Hive kimlik do\u011frulama kodunuzu girin. \n\n Ba\u015fka bir kod istemek i\u00e7in l\u00fctfen 0000 kodunu girin.", "title": "Hive \u0130ki Fakt\u00f6rl\u00fc Kimlik Do\u011frulama." }, + "configuration": { + "data": { + "device_name": "Cihaz ad\u0131" + }, + "description": "Hive yap\u0131land\u0131rman\u0131z\u0131 girin", + "title": "Hive Yap\u0131land\u0131rmas\u0131." + }, "reauth": { "data": { "password": "Parola", @@ -34,7 +41,7 @@ "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Hive oturum a\u00e7ma bilgilerinizi ve yap\u0131land\u0131rman\u0131z\u0131 girin.", + "description": "Hive giri\u015f bilgilerinizi girin.", "title": "Hive Giri\u015f yap" } } diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index ddd9c5da359..c695f5524c3 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -96,29 +96,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.warning("HLK-SW16 %s connected", address) async_dispatcher_send(hass, f"hlk_sw16_device_available_{entry.entry_id}", True) - async def connect(): - """Set up connection and hook it into HA for reconnect/shutdown.""" - _LOGGER.info("Initiating HLK-SW16 connection to %s", address) + _LOGGER.debug("Initiating HLK-SW16 connection to %s", address) - client = await create_hlk_sw16_connection( - host=host, - port=port, - disconnect_callback=disconnected, - reconnect_callback=reconnected, - loop=hass.loop, - timeout=CONNECTION_TIMEOUT, - reconnect_interval=DEFAULT_RECONNECT_INTERVAL, - keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, - ) + client = await create_hlk_sw16_connection( + host=host, + port=port, + disconnect_callback=disconnected, + reconnect_callback=reconnected, + loop=hass.loop, + timeout=CONNECTION_TIMEOUT, + reconnect_interval=DEFAULT_RECONNECT_INTERVAL, + keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, + ) - hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client + hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client - # Load entities - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Load entities + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - _LOGGER.info("Connected to HLK-SW16 device: %s", address) - - hass.loop.create_task(connect()) + _LOGGER.debug("Connected to HLK-SW16 device: %s", address) return True diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index f57c7aeb8af..6e664ad07e4 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -103,39 +103,36 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR, Platform.S def _get_appliance_by_device_id( hass: HomeAssistant, device_id: str -) -> api.HomeConnectDevice | None: +) -> api.HomeConnectDevice: """Return a Home Connect appliance instance given an device_id.""" for hc_api in hass.data[DOMAIN].values(): for dev_dict in hc_api.devices: device = dev_dict[CONF_DEVICE] if device.device_id == device_id: return device.appliance - _LOGGER.error("Appliance for device id %s not found", device_id) - return None + raise ValueError(f"Appliance for device id {device_id} not found") async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Home Connect component.""" hass.data[DOMAIN] = {} - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of Home Connect integration in YAML is deprecated and " - "will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) + if DOMAIN in config: + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential( + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + ), + ) + _LOGGER.warning( + "Configuration of Home Connect integration in YAML is deprecated and " + "will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) async def _async_service_program(call, method): """Execute calls to services taking a program.""" @@ -148,18 +145,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: } appliance = _get_appliance_by_device_id(hass, device_id) - if appliance is not None: - await hass.async_add_executor_job( - getattr(appliance, method), program, options - ) + await hass.async_add_executor_job(getattr(appliance, method), program, options) async def _async_service_command(call, command): """Execute calls to services executing a command.""" device_id = call.data[ATTR_DEVICE_ID] appliance = _get_appliance_by_device_id(hass, device_id) - if appliance is not None: - await hass.async_add_executor_job(appliance.execute_command, command) + await hass.async_add_executor_job(appliance.execute_command, command) async def _async_service_key_value(call, method): """Execute calls to services taking a key and value.""" @@ -169,20 +162,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: device_id = call.data[ATTR_DEVICE_ID] appliance = _get_appliance_by_device_id(hass, device_id) - if appliance is not None: - if unit is not None: - await hass.async_add_executor_job( - getattr(appliance, method), - key, - value, - unit, - ) - else: - await hass.async_add_executor_job( - getattr(appliance, method), - key, - value, - ) + if unit is not None: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + unit, + ) + else: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + ) async def async_service_option_active(call): """Service for setting an option for an active program.""" @@ -269,7 +261,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await update_all_devices(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index 00d759b47d5..f3c98e618b8 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -113,7 +113,6 @@ class HomeConnectDevice: """Initialize the device class.""" self.hass = hass self.appliance = appliance - self.entities = [] def initialize(self): """Fetch the info needed to initialize the device.""" diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index 60a0c3974cd..b27988f997d 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -20,7 +20,6 @@ class HomeConnectEntity(Entity): self.device = device self.desc = desc self._name = f"{self.device.appliance.name} {desc}" - self.device.entities.append(self) async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index 78e31c83caa..d58086e59ec 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -1,5 +1,4 @@ """The Legrand Home+ Control integration.""" -import asyncio from datetime import timedelta import logging @@ -9,7 +8,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, @@ -83,7 +82,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = hass_entry_data[API] = HomePlusControlAsyncApi(hass, entry, implementation) # Set of entity unique identifiers of this integration - uids = hass_entry_data[ENTITY_UIDS] = set() + uids: set[str] = set() + hass_entry_data[ENTITY_UIDS] = uids # Integration dispatchers hass_entry_data[DISPATCHER_REMOVERS] = [] @@ -101,12 +101,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(10): - module_data = await api.async_get_modules() + return await api.async_get_modules() except HomePlusControlApiError as err: raise UpdateFailed( f"Error communicating with API: {err} [{type(err)}]" ) from err + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="home_plus_control_module", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=300), + ) + hass_entry_data[DATA_COORDINATOR] = coordinator + + @callback + def _async_update_entities(): + """Process entities and add or remove them based after an update.""" + if not (module_data := coordinator.data): + return + # Remove obsolete entities from Home Assistant entity_uids_to_remove = uids - set(module_data) for uid in entity_uids_to_remove: @@ -125,31 +142,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator, ) - return module_data + entry.async_on_unload(coordinator.async_add_listener(_async_update_entities)) - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name="home_plus_control_module", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=300), - ) - hass_entry_data[DATA_COORDINATOR] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - async def start_platforms(): - """Continue setting up the platforms.""" - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - # Only refresh the coordinator after all platforms are loaded. - await coordinator.async_refresh() - - hass.async_create_task(start_platforms()) + # Only refresh the coordinator after all platforms are loaded. + await coordinator.async_refresh() return True diff --git a/homeassistant/components/home_plus_control/api.py b/homeassistant/components/home_plus_control/api.py index d9db95323de..9f092b28920 100644 --- a/homeassistant/components/home_plus_control/api.py +++ b/homeassistant/components/home_plus_control/api.py @@ -5,6 +5,7 @@ from homeassistant import config_entries, core from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from .const import DEFAULT_UPDATE_INTERVALS +from .helpers import HomePlusControlOAuth2Implementation class HomePlusControlAsyncApi(HomePlusControlAPI): @@ -40,6 +41,8 @@ class HomePlusControlAsyncApi(HomePlusControlAPI): hass, config_entry, implementation ) + assert isinstance(implementation, HomePlusControlOAuth2Implementation) + # Create the API authenticated client - external library super().__init__( subscription_key=implementation.subscription_key, diff --git a/homeassistant/components/home_plus_control/translations/ja.json b/homeassistant/components/home_plus_control/translations/ja.json index 3cef1cd5fa1..e1d0ade87b9 100644 --- a/homeassistant/components/home_plus_control/translations/ja.json +++ b/homeassistant/components/home_plus_control/translations/ja.json @@ -6,7 +6,7 @@ "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json index e9c91d9df20..8bcf5d96a86 100644 --- a/homeassistant/components/homeassistant/translations/ca.json +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arquitectura de la CPU", + "config_dir": "Directori de configuraci\u00f3", "dev": "Desenvolupador", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json index 54909cb3c24..756670e5a47 100644 --- a/homeassistant/components/homeassistant/translations/de.json +++ b/homeassistant/components/homeassistant/translations/de.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU-Architektur", + "config_dir": "Konfigurationsverzeichnis", "dev": "Entwicklung", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/el.json b/homeassistant/components/homeassistant/translations/el.json index 616ad96a867..9c345d511e1 100644 --- a/homeassistant/components/homeassistant/translations/el.json +++ b/homeassistant/components/homeassistant/translations/el.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "\u0391\u03c1\u03c7\u03b9\u03c4\u03b5\u03ba\u03c4\u03bf\u03bd\u03b9\u03ba\u03ae CPU", + "config_dir": "\u039a\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd", "dev": "\u0391\u03bd\u03ac\u03c0\u03c4\u03c5\u03be\u03b7", "docker": "Docker", "hassio": "\u0395\u03c0\u03cc\u03c0\u03c4\u03b7\u03c2", diff --git a/homeassistant/components/homeassistant/translations/et.json b/homeassistant/components/homeassistant/translations/et.json index 529b84120d7..7b9b675ed6f 100644 --- a/homeassistant/components/homeassistant/translations/et.json +++ b/homeassistant/components/homeassistant/translations/et.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Protsessori arhitektuur", + "config_dir": "Konfiguratsiooni kaust", "dev": "Arendus", "docker": "Docker", "hassio": "Haldur", diff --git a/homeassistant/components/homeassistant/translations/fr.json b/homeassistant/components/homeassistant/translations/fr.json index ae9dfb0a7da..48e38978ba6 100644 --- a/homeassistant/components/homeassistant/translations/fr.json +++ b/homeassistant/components/homeassistant/translations/fr.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Architecture du processeur", + "config_dir": "R\u00e9pertoire de configuration", "dev": "D\u00e9veloppement", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index b4da84596bf..7261dfa1f7a 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Processzor architekt\u00fara", + "config_dir": "Konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r", "dev": "Fejleszt\u00e9s", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/id.json b/homeassistant/components/homeassistant/translations/id.json index f795a47ee20..7c2994d8bbb 100644 --- a/homeassistant/components/homeassistant/translations/id.json +++ b/homeassistant/components/homeassistant/translations/id.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arsitektur CPU", + "config_dir": "Direktori Konfigurasi", "dev": "Pengembangan", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index 3052a536338..432fb9dea46 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Architettura della CPU", + "config_dir": "Cartella di configurazione", "dev": "Sviluppo", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json index 14b1deb55c8..8be012c7c1a 100644 --- a/homeassistant/components/homeassistant/translations/ja.json +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU\u30a2\u30fc\u30ad\u30c6\u30af\u30c1\u30e3", + "config_dir": "\u30b3\u30f3\u30d5\u30a3\u30ae\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u30c7\u30a3\u30ec\u30af\u30c8\u30ea", "dev": "\u30c7\u30a3\u30d9\u30ed\u30c3\u30d7\u30e1\u30f3\u30c8", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index 1037d161c2b..0bafc7a3959 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU-architectuur", + "config_dir": "Configuratiemap", "dev": "Ontwikkeling", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/pl.json b/homeassistant/components/homeassistant/translations/pl.json index 9f85cc4ff15..bdf26a8b49d 100644 --- a/homeassistant/components/homeassistant/translations/pl.json +++ b/homeassistant/components/homeassistant/translations/pl.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Architektura procesora", + "config_dir": "Folder konfiguracji", "dev": "Wersja deweloperska", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/pt-BR.json b/homeassistant/components/homeassistant/translations/pt-BR.json index f30a1775e0e..5ea540d67f3 100644 --- a/homeassistant/components/homeassistant/translations/pt-BR.json +++ b/homeassistant/components/homeassistant/translations/pt-BR.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arquitetura da CPU", + "config_dir": "Diret\u00f3rio de configura\u00e7\u00e3o", "dev": "Desenvolvimento", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json index 13fd384d6a2..30f84823c6b 100644 --- a/homeassistant/components/homeassistant/translations/pt.json +++ b/homeassistant/components/homeassistant/translations/pt.json @@ -10,6 +10,7 @@ "os_version": "Vers\u00e3o do Sistema Operativo", "python_version": "Vers\u00e3o Python", "timezone": "Fuso hor\u00e1rio", + "user": "Utilizador", "version": "Vers\u00e3o", "virtualenv": "Ambiente Virtual" } diff --git a/homeassistant/components/homeassistant/translations/ru.json b/homeassistant/components/homeassistant/translations/ru.json index f8932f1ea7d..b0e27b70861 100644 --- a/homeassistant/components/homeassistant/translations/ru.json +++ b/homeassistant/components/homeassistant/translations/ru.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", + "config_dir": "\u041a\u0430\u0442\u0430\u043b\u043e\u0433 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", "dev": "\u0421\u0440\u0435\u0434\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index cffbee33e74..7ec8074702b 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU Mimarisi", + "config_dir": "Yap\u0131land\u0131rma Dizini", "dev": "Geli\u015ftirme", "docker": "Docker", "hassio": "S\u00fcperviz\u00f6r", diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json index 03812830b26..c42acf960c6 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hant.json +++ b/homeassistant/components/homeassistant/translations/zh-Hant.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU \u67b6\u69cb", + "config_dir": "\u8a2d\u5b9a\u76ee\u9304", "dev": "\u958b\u767c\u7248", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py new file mode 100644 index 00000000000..60386e3d080 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -0,0 +1,188 @@ +"""The Home Assistant alerts integration.""" +from __future__ import annotations + +import asyncio +import dataclasses +from datetime import timedelta +import logging + +import aiohttp +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.components.repairs import async_create_issue, async_delete_issue +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.const import __version__ +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.start import async_at_start +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.yaml import parse_yaml + +DOMAIN = "homeassistant_alerts" +UPDATE_INTERVAL = timedelta(hours=3) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up alerts.""" + last_alerts: dict[str, str | None] = {} + + async def async_update_alerts() -> None: + nonlocal last_alerts + + active_alerts: dict[str, str | None] = {} + + for issue_id, alert in coordinator.data.items(): + # Skip creation if already created and not updated since then + if issue_id in last_alerts and alert.date_updated == last_alerts[issue_id]: + active_alerts[issue_id] = alert.date_updated + continue + + # Fetch alert to get title + description + try: + response = await async_get_clientsession(hass).get( + f"https://alerts.home-assistant.io/alerts/{alert.filename}", + timeout=aiohttp.ClientTimeout(total=10), + ) + except asyncio.TimeoutError: + _LOGGER.warning("Error fetching %s: timeout", alert.filename) + continue + + alert_content = await response.text() + alert_parts = alert_content.split("---") + + if len(alert_parts) != 3: + _LOGGER.warning( + "Error parsing %s: unexpected metadata format", alert.filename + ) + continue + + try: + alert_info = parse_yaml(alert_parts[1]) + except ValueError as err: + _LOGGER.warning("Error parsing %s metadata: %s", alert.filename, err) + continue + + if not isinstance(alert_info, dict) or "title" not in alert_info: + _LOGGER.warning("Error in %s metadata: title not found", alert.filename) + continue + + alert_title = alert_info["title"] + alert_content = alert_parts[2].strip() + + async_create_issue( + hass, + DOMAIN, + issue_id, + is_fixable=False, + issue_domain=alert.integration, + severity=IssueSeverity.WARNING, + translation_key="alert", + translation_placeholders={ + "title": alert_title, + "description": alert_content, + }, + ) + active_alerts[issue_id] = alert.date_updated + + inactive_alerts = last_alerts.keys() - active_alerts.keys() + for issue_id in inactive_alerts: + async_delete_issue(hass, DOMAIN, issue_id) + + last_alerts = active_alerts + + @callback + def async_schedule_update_alerts() -> None: + if not coordinator.last_update_success: + return + + hass.async_create_task(async_update_alerts()) + + coordinator = AlertUpdateCoordinator(hass) + coordinator.async_add_listener(async_schedule_update_alerts) + + async def initial_refresh(hass: HomeAssistant) -> None: + await coordinator.async_refresh() + + async_at_start(hass, initial_refresh) + + return True + + +@dataclasses.dataclass(frozen=True) +class IntegrationAlert: + """Issue Registry Entry.""" + + integration: str + filename: str + date_updated: str | None + + @property + def issue_id(self) -> str: + """Return the issue id.""" + return f"{self.filename}_{self.integration}" + + +class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]): + """Data fetcher for HA Alerts.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the data updater.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=UPDATE_INTERVAL, + ) + self.ha_version = AwesomeVersion( + __version__, + ensure_strategy=AwesomeVersionStrategy.CALVER, + find_first_match=False, + ) + + async def _async_update_data(self) -> dict[str, IntegrationAlert]: + response = await async_get_clientsession(self.hass).get( + "https://alerts.home-assistant.io/alerts.json", + timeout=aiohttp.ClientTimeout(total=10), + ) + alerts = await response.json() + + result = {} + + for alert in alerts: + if "integrations" not in alert: + continue + + if "homeassistant" in alert: + if "affected_from_version" in alert["homeassistant"]: + affected_from_version = AwesomeVersion( + alert["homeassistant"]["affected_from_version"], + find_first_match=False, + ) + if self.ha_version < affected_from_version: + continue + if "resolved_in_version" in alert["homeassistant"]: + resolved_in_version = AwesomeVersion( + alert["homeassistant"]["resolved_in_version"], + find_first_match=False, + ) + if self.ha_version >= resolved_in_version: + continue + + for integration in alert["integrations"]: + if "package" not in integration: + continue + + if integration["package"] not in self.hass.config.components: + continue + + integration_alert = IntegrationAlert( + integration=integration["package"], + filename=alert["filename"], + date_updated=alert.get("date_updated"), + ) + + result[integration_alert.issue_id] = integration_alert + + return result diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json new file mode 100644 index 00000000000..7c9ddf4f905 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "homeassistant_alerts", + "name": "Home Assistant Alerts", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts", + "codeowners": ["@home-assistant/core"], + "dependencies": ["repairs"] +} diff --git a/homeassistant/components/homeassistant_alerts/strings.json b/homeassistant/components/homeassistant_alerts/strings.json new file mode 100644 index 00000000000..7a9634d6268 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "title": "{title}", + "description": "{description}" + } + } +} diff --git a/homeassistant/components/homeassistant_alerts/translations/de.json b/homeassistant/components/homeassistant_alerts/translations/de.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/en.json b/homeassistant/components/homeassistant_alerts/translations/en.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/it.json b/homeassistant/components/homeassistant_alerts/translations/it.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pl.json b/homeassistant/components/homeassistant_alerts/translations/pl.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pt-BR.json b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 5d7e647e466..6852ff4fa9f 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -8,7 +8,6 @@ import ipaddress import logging import os from typing import Any, cast -from uuid import UUID from aiohttp import web from pyhap.const import STANDALONE_AID @@ -510,7 +509,7 @@ class HomeKit: self.bridge: HomeBridge | None = None - def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: UUID) -> None: + def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: """Set up bridge and accessory driver.""" persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 4086db071c2..3aefb01d63a 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -44,14 +44,14 @@ "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domini}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse ad eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", "title": "Seleziona le entit\u00e0 da escludere" }, "include": { "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domini}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a meno che non siano selezionate entit\u00e0 specifiche.", "title": "Seleziona le entit\u00e0 da includere" }, "init": { diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json index f122a97b19c..931dfcdd1c9 100644 --- a/homeassistant/components/homekit/translations/pt.json +++ b/homeassistant/components/homekit/translations/pt.json @@ -20,8 +20,14 @@ "cameras": { "title": "Selecione o codec de v\u00eddeo da c\u00e2mera." }, + "exclude": { + "data": { + "entities": "Entidades" + } + }, "init": { "data": { + "domains": "Dom\u00ednios a incluir", "mode": "Modo" }, "title": "Selecione os dom\u00ednios a serem expostos." diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 6909b226556..37dd648dedb 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -6,6 +6,11 @@ import logging from typing import Any import aiohomekit +from aiohomekit.exceptions import ( + AccessoryDisconnectedError, + AccessoryNotFoundError, + EncryptionError, +) from aiohomekit.model import Accessory from aiohomekit.model.characteristics import ( Characteristic, @@ -26,8 +31,8 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice, valid_serial_number from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS -from .storage import EntityMapStorage -from .utils import async_get_controller +from .storage import async_get_entity_storage +from .utils import async_get_controller, folded_name _LOGGER = logging.getLogger(__name__) @@ -42,6 +47,7 @@ class HomeKitEntity(Entity): self._accessory = accessory self._aid = devinfo["aid"] self._iid = devinfo["iid"] + self._char_name: str | None = None self._features = 0 self.setup() @@ -75,7 +81,9 @@ class HomeKitEntity(Entity): ) self._accessory.add_pollable_characteristics(self.pollable_characteristics) - self._accessory.add_watchable_characteristics(self.watchable_characteristics) + await self._accessory.add_watchable_characteristics( + self.watchable_characteristics + ) async def async_will_remove_from_hass(self) -> None: """Prepare to be removed from hass.""" @@ -127,6 +135,9 @@ class HomeKitEntity(Entity): if CharacteristicPermissions.events in char.perms: self.watchable_characteristics.append((self._aid, char.iid)) + if self._char_name is None: + self._char_name = char.service.value(CharacteristicsTypes.NAME) + @property def unique_id(self) -> str: """Return the ID of this device.""" @@ -137,10 +148,31 @@ class HomeKitEntity(Entity): # Some accessories do not have a serial number return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" + @property + def default_name(self) -> str | None: + """Return the default name of the device.""" + return None + @property def name(self) -> str | None: """Return the name of the device if any.""" - return self.accessory.name + accessory_name = self.accessory.name + # If the service has a name char, use that, if not + # fallback to the default name provided by the subclass + device_name = self._char_name or self.default_name + folded_device_name = folded_name(device_name or "") + folded_accessory_name = folded_name(accessory_name) + if device_name: + # Sometimes the device name includes the accessory + # name already like My ecobee Occupancy / My ecobee + if folded_device_name.startswith(folded_accessory_name): + return device_name + if ( + folded_accessory_name not in folded_device_name + and folded_device_name not in folded_accessory_name + ): + return f"{accessory_name} {device_name}" + return accessory_name @property def available(self) -> bool: @@ -200,17 +232,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, unique_id=normalize_hkid(conn.unique_id) ) - if not await conn.async_setup(): + try: + await conn.async_setup() + except (AccessoryNotFoundError, EncryptionError, AccessoryDisconnectedError) as ex: del hass.data[KNOWN_DEVICES][conn.unique_id] - raise ConfigEntryNotReady + await conn.pairing.close() + raise ConfigEntryNotReady from ex return True async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up for Homekit devices.""" - map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) - await map_storage.async_initialize() + await async_get_entity_storage(hass) await async_get_controller(hass) diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index 7d2c737b509..e9c85dbe876 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -106,7 +106,7 @@ class HomeKitButton(CharacteristicEntity, ButtonEntity): @property def name(self) -> str: """Return the name of the device if any.""" - if name := super().name: + if name := self.accessory.name: return f"{name} {self.entity_description.name}" return f"{self.entity_description.name}" diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index 0ffa0a22f4d..0f0dd4f9050 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -25,7 +25,7 @@ class HomeKitCamera(AccessoryEntity, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a jpeg with the current camera snapshot.""" - return await self._accessory.pairing.image( + return await self._accessory.pairing.image( # type: ignore[attr-defined] self._aid, width or 640, height or 480, diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 44ecee13875..b76ed1ea6a9 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -25,6 +25,8 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + FAN_AUTO, + FAN_ON, SWING_OFF, SWING_VERTICAL, ClimateEntityFeature, @@ -72,6 +74,7 @@ TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS = { TargetHeaterCoolerStateValues.COOL: HVACMode.COOL, } + # Map of hass operation modes to homekit modes MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} @@ -104,19 +107,65 @@ async def async_setup_entry( conn.add_listener(async_add_service) -class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): - """Representation of a Homekit climate device.""" +class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity): + """The base HomeKit Controller climate entity.""" + + _attr_temperature_unit = TEMP_CELSIUS def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ + CharacteristicsTypes.TEMPERATURE_CURRENT, + CharacteristicsTypes.FAN_STATE_TARGET, + ] + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) + + @property + def fan_modes(self) -> list[str] | None: + """Return the available fan modes.""" + if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET): + return [FAN_ON, FAN_AUTO] + return None + + @property + def fan_mode(self) -> str | None: + """Return the current fan mode.""" + fan_mode = self.service.value(CharacteristicsTypes.FAN_STATE_TARGET) + return FAN_AUTO if fan_mode else FAN_ON + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Turn fan to manual/auto.""" + await self.async_put_characteristics( + {CharacteristicsTypes.FAN_STATE_TARGET: int(fan_mode == FAN_AUTO)} + ) + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + features = 0 + + if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET): + features |= ClimateEntityFeature.FAN_MODE + + return features + + +class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): + """Representation of a Homekit climate device.""" + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity cares about.""" + return super().get_characteristic_types() + [ CharacteristicsTypes.ACTIVE, CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE, CharacteristicsTypes.TARGET_HEATER_COOLER_STATE, CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, CharacteristicsTypes.SWING_MODE, - CharacteristicsTypes.TEMPERATURE_CURRENT, ] async def async_set_temperature(self, **kwargs: Any) -> None: @@ -162,11 +211,6 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): } ) - @property - def current_temperature(self) -> float: - """Return the current temperature.""" - return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) - @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" @@ -321,7 +365,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): @property def supported_features(self) -> int: """Return the list of supported features.""" - features = 0 + features = super().supported_features if self.service.has(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD): features |= ClimateEntityFeature.TARGET_TEMPERATURE @@ -334,22 +378,16 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): return features - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - -class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): +class HomeKitClimateEntity(HomeKitBaseClimateEntity): """Representation of a Homekit climate device.""" def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" - return [ + return super().get_characteristic_types() + [ CharacteristicsTypes.HEATING_COOLING_CURRENT, CharacteristicsTypes.HEATING_COOLING_TARGET, CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, - CharacteristicsTypes.TEMPERATURE_CURRENT, CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_TARGET, CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, @@ -411,11 +449,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): } ) - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) - @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" @@ -558,7 +591,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): @property def supported_features(self) -> int: """Return the list of supported features.""" - features = 0 + features = super().supported_features if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): features |= ClimateEntityFeature.TARGET_TEMPERATURE @@ -573,11 +606,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): return features - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - ENTITY_TYPES = { ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity, diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index b4bd66aa626..31677e37b20 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,13 +1,18 @@ """Config flow to configure homekit_controller.""" from __future__ import annotations +from collections.abc import Awaitable import logging import re -from typing import Any +from typing import TYPE_CHECKING, Any, cast import aiohomekit +from aiohomekit import Controller, const as aiohomekit_const +from aiohomekit.controller.abstract import AbstractDiscovery, AbstractPairing from aiohomekit.exceptions import AuthenticationError -from aiohomekit.model import Accessories, CharacteristicsTypes, ServicesTypes +from aiohomekit.model.categories import Categories +from aiohomekit.model.status_flags import StatusFlags +from aiohomekit.utils import domain_supported, domain_to_name import voluptuous as vol from homeassistant import config_entries @@ -16,9 +21,15 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr +from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES +from .storage import async_get_entity_storage from .utils import async_get_controller +if TYPE_CHECKING: + from homeassistant.components import bluetooth + + HOMEKIT_DIR = ".homekit" HOMEKIT_BRIDGE_DOMAIN = "homekit" @@ -38,6 +49,8 @@ PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") _LOGGER = logging.getLogger(__name__) +BLE_DEFAULT_NAME = "Bluetooth device" + INSECURE_CODES = { "00000000", "11111111", @@ -59,6 +72,11 @@ def normalize_hkid(hkid: str) -> str: return hkid.lower() +def formatted_category(category: Categories) -> str: + """Return a human readable category name.""" + return str(category.name).replace("_", " ").title() + + @callback def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None: """Return a set of the configured hosts.""" @@ -89,14 +107,15 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the homekit_controller flow.""" - self.model = None - self.hkid = None - self.name = None - self.devices = {} - self.controller = None - self.finish_pairing = None + self.model: str | None = None + self.hkid: str | None = None + self.name: str | None = None + self.category: Categories | None = None + self.devices: dict[str, AbstractDiscovery] = {} + self.controller: Controller | None = None + self.finish_pairing: Awaitable[AbstractPairing] | None = None async def _async_setup_controller(self): """Create the controller.""" @@ -108,9 +127,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: key = user_input["device"] - self.hkid = self.devices[key].description.id - self.model = self.devices[key].description.model - self.name = self.devices[key].description.name + discovery = self.devices[key] + self.category = discovery.description.category + self.hkid = discovery.description.id + self.model = getattr(discovery.description, "model", BLE_DEFAULT_NAME) + self.name = discovery.description.name or BLE_DEFAULT_NAME await self.async_set_unique_id( normalize_hkid(self.hkid), raise_on_progress=False @@ -135,7 +156,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", errors=errors, data_schema=vol.Schema( - {vol.Required("device"): vol.In(self.devices.keys())} + { + vol.Required("device"): vol.In( + { + key: f"{key} ({formatted_category(discovery.description.category)})" + for key, discovery in self.devices.items() + } + ) + } ), ) @@ -148,13 +176,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self._async_setup_controller() try: - device = await self.controller.async_find(unique_id) + discovery = await self.controller.async_find(unique_id) except aiohomekit.AccessoryNotFoundError: return self.async_abort(reason="accessory_not_found_error") - self.name = device.description.name - self.model = device.description.model - self.hkid = device.description.id + self.name = discovery.description.name + self.model = discovery.description.model + self.category = discovery.description.category + self.hkid = discovery.description.id return self._async_step_pair_show_form() @@ -203,9 +232,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): hkid = properties[zeroconf.ATTR_PROPERTIES_ID] normalized_hkid = normalize_hkid(hkid) + # If this aiohomekit doesn't support this particular device, ignore it. + if not domain_supported(discovery_info.name): + return self.async_abort(reason="ignored_model") + model = properties["md"] - name = discovery_info.name.replace("._hap._tcp.local.", "") + name = domain_to_name(discovery_info.name) status_flags = int(properties["sf"]) + category = Categories(int(properties.get("ci", 0))) paired = not status_flags & 0x01 # The configuration number increases every time the characteristic map @@ -235,17 +269,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( existing_entry, data={**existing_entry.data, **updated_ip_port} ) - conn = self.hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = self.hass.data[KNOWN_DEVICES][hkid] # When we rediscover the device, let aiohomekit know # that the device is available and we should not wait # to retry connecting any longer. reconnect_soon # will do nothing if the device is already connected - await conn.pairing.connection.reconnect_soon() - if conn.config_num != config_num: + await conn.pairing.reconnect_soon() + if config_num and conn.config_num != config_num: _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid ) - self.hass.async_create_task(conn.async_refresh_entity_map(config_num)) + conn.async_notify_config_changed(config_num) return self.async_abort(reason="already_configured") _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) @@ -319,6 +353,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.name = name self.model = model + self.category = category self.hkid = hkid # We want to show the pairing form - but don't call async_step_pair @@ -326,6 +361,55 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # pairing code) return self._async_step_pair_show_form() + async def async_step_bluetooth( + self, discovery_info: bluetooth.BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED: + return self.async_abort(reason="ignored_model") + + # Late imports in case BLE is not available + from aiohomekit.controller.ble.discovery import ( # pylint: disable=import-outside-toplevel + BleDiscovery, + ) + from aiohomekit.controller.ble.manufacturer_data import ( # pylint: disable=import-outside-toplevel + HomeKitAdvertisement, + ) + + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + + mfr_data = discovery_info.manufacturer_data + + try: + device = HomeKitAdvertisement.from_manufacturer_data( + discovery_info.name, discovery_info.address, mfr_data + ) + except ValueError: + return self.async_abort(reason="ignored_model") + + if not (device.status_flags & StatusFlags.UNPAIRED): + return self.async_abort(reason="already_paired") + + if self.controller is None: + await self._async_setup_controller() + assert self.controller is not None + + try: + discovery = await self.controller.async_find(device.id) + except aiohomekit.AccessoryNotFoundError: + return self.async_abort(reason="accessory_not_found_error") + + if TYPE_CHECKING: + discovery = cast(BleDiscovery, discovery) + + self.name = discovery.description.name + self.model = BLE_DEFAULT_NAME + self.category = discovery.description.category + self.hkid = discovery.description.id + + return self._async_step_pair_show_form() + async def async_step_pair(self, pair_info=None): """Pair with a new HomeKit accessory.""" # If async_step_pair is called with no pairing code then we do the M1 @@ -446,8 +530,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_step_pair_show_form(self, errors=None): - placeholders = {"name": self.name} - self.context["title_placeholders"] = {"name": self.name} + placeholders = self.context["title_placeholders"] = { + "name": self.name, + "category": formatted_category(self.category), + } schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)} if errors and errors.get("pairing_code") == "insecure_setup_code": @@ -460,26 +546,33 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema(schema), ) - async def _entry_from_accessory(self, pairing): + async def _entry_from_accessory(self, pairing: AbstractPairing) -> FlowResult: """Return a config entry from an initialized bridge.""" # The bulk of the pairing record is stored on the config entry. # A specific exception is the 'accessories' key. This is more # volatile. We do cache it, but not against the config entry. # So copy the pairing data and mutate the copy. - pairing_data = pairing.pairing_data.copy() + pairing_data = pairing.pairing_data.copy() # type: ignore[attr-defined] # Use the accessories data from the pairing operation if it is # available. Otherwise request a fresh copy from the API. # This removes the 'accessories' key from pairing_data at # the same time. - if not (accessories := pairing_data.pop("accessories", None)): - accessories = await pairing.list_accessories_and_characteristics() + name = await pairing.get_primary_name() - parsed = Accessories.from_list(accessories) - accessory_info = parsed.aid(1).services.first( - service_type=ServicesTypes.ACCESSORY_INFORMATION + await pairing.close() + + # Save the state of the accessories so we do not + # have to request them again when we setup the + # config entry. + accessories_state = pairing.accessories_state + entity_storage = await async_get_entity_storage(self.hass) + assert self.unique_id is not None + entity_storage.async_create_or_update_map( + self.unique_id, + accessories_state.config_num, + accessories_state.accessories.serialize(), ) - name = accessory_info.value(CharacteristicsTypes.NAME, "") return self.async_create_entry(title=name, data=pairing_data) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 7a85e234807..b2f3b3ae8e0 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -3,21 +3,24 @@ from __future__ import annotations import asyncio from collections.abc import Callable -import datetime +from datetime import timedelta import logging +from types import MappingProxyType from typing import Any +from aiohomekit import Controller from aiohomekit.exceptions import ( AccessoryDisconnectedError, AccessoryNotFoundError, EncryptionError, ) -from aiohomekit.model import Accessories, Accessory +from aiohomekit.model import Accessories, Accessory, Transport from aiohomekit.model.characteristics import Characteristic from aiohomekit.model.services import Service +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_VIA_DEVICE -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo @@ -35,10 +38,11 @@ from .const import ( IDENTIFIER_SERIAL_NUMBER, ) from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry +from .storage import EntityMapStorage -DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 +BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds _LOGGER = logging.getLogger(__name__) @@ -60,7 +64,12 @@ def valid_serial_number(serial: str) -> bool: class HKDevice: """HomeKit device.""" - def __init__(self, hass, config_entry, pairing_data) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + pairing_data: MappingProxyType[str, Any], + ) -> None: """Initialise a generic HomeKit device.""" self.hass = hass @@ -70,15 +79,12 @@ class HKDevice: # don't want to mutate a dict owned by a config entry. self.pairing_data = pairing_data.copy() - self.pairing = hass.data[CONTROLLER].load_pairing( + connection: Controller = hass.data[CONTROLLER] + + self.pairing = connection.load_pairing( self.pairing_data["AccessoryPairingID"], self.pairing_data ) - self.accessories = None - self.config_num = 0 - - self.entity_map = Accessories() - # A list of callbacks that turn HK accessories into entities self.accessory_factories: list[AddAccessoryCb] = [] @@ -116,6 +122,7 @@ class HKDevice: # If this is set polling is active and can be disabled by calling # this method. self._polling_interval_remover: CALLBACK_TYPE | None = None + self._ble_available_interval_remover: CALLBACK_TYPE | None = None # Never allow concurrent polling of the same accessory or bridge self._polling_lock = asyncio.Lock() @@ -127,7 +134,15 @@ class HKDevice: self.watchable_characteristics: list[tuple[int, int]] = [] - self.pairing.dispatcher_connect(self.process_new_events) + @property + def entity_map(self) -> Accessories: + """Return the accessories from the pairing.""" + return self.pairing.accessories_state.accessories + + @property + def config_num(self) -> int: + """Return the config num from the pairing.""" + return self.pairing.accessories_state.config_num def add_pollable_characteristics( self, characteristics: list[tuple[int, int]] @@ -141,12 +156,12 @@ class HKDevice: char for char in self.pollable_characteristics if char[0] != accessory_id ] - def add_watchable_characteristics( + async def add_watchable_characteristics( self, characteristics: list[tuple[int, int]] ) -> None: """Add (aid, iid) pairs that we need to poll.""" self.watchable_characteristics.extend(characteristics) - self.hass.async_create_task(self.pairing.subscribe(characteristics)) + await self.pairing.subscribe(characteristics) def remove_watchable_characteristics(self, accessory_id: int) -> None: """Remove all pollable characteristics by accessory id.""" @@ -165,29 +180,71 @@ class HKDevice: self.available = available async_dispatcher_send(self.hass, self.signal_state_updated) - async def async_setup(self) -> bool: + async def async_setup(self) -> None: """Prepare to use a paired HomeKit device in Home Assistant.""" - cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id) - if not cache: - if await self.async_refresh_entity_map(self.config_num): - self._polling_interval_remover = async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL - ) - return True - return False + entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] + pairing = self.pairing + transport = pairing.transport + entry = self.config_entry - self.accessories = cache["accessories"] - self.config_num = cache["config_num"] + if cache := entity_storage.get_map(self.unique_id): + pairing.restore_accessories_state(cache["accessories"], cache["config_num"]) - self.entity_map = Accessories.from_list(self.accessories) + # We need to force an update here to make sure we have + # the latest values since the async_update we do in + # async_process_entity_map will no values to poll yet + # since entities are added via dispatching and then + # they add the chars they are concerned about in + # async_added_to_hass which is too late. + # + # Ideally we would know which entities we are about to add + # so we only poll those chars but that is not possible + # yet. + try: + await self.pairing.async_populate_accessories_state(force_update=True) + except AccessoryNotFoundError: + if transport != Transport.BLE or not cache: + # BLE devices may sleep and we can't force a connection + raise - self._polling_interval_remover = async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL + entry.async_on_unload(pairing.dispatcher_connect(self.process_new_events)) + entry.async_on_unload( + pairing.dispatcher_connect_config_changed(self.process_config_changed) + ) + entry.async_on_unload( + pairing.dispatcher_availability_changed(self.async_set_available_state) ) - self.hass.async_create_task(self.async_process_entity_map()) + await self.async_process_entity_map() - return True + if not cache: + # If its missing from the cache, make sure we save it + self.async_save_entity_map() + # If everything is up to date, we can create the entities + # since we know the data is not stale. + await self.async_add_new_entities() + + self.async_set_available_state(self.pairing.is_available) + + self._polling_interval_remover = async_track_time_interval( + self.hass, self.async_update, self.pairing.poll_interval + ) + + if transport == Transport.BLE: + # If we are using BLE, we need to periodically check of the + # BLE device is available since we won't get callbacks + # when it goes away since we HomeKit supports disconnected + # notifications and we cannot treat a disconnect as unavailability. + self._ble_available_interval_remover = async_track_time_interval( + self.hass, + self.async_update_available_state, + timedelta(seconds=BLE_AVAILABILITY_CHECK_INTERVAL), + ) + + async def async_add_new_entities(self) -> None: + """Add new entities to Home Assistant.""" + await self.async_load_platforms() + self.add_entities() def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo: """Build a DeviceInfo for a given accessory.""" @@ -360,30 +417,16 @@ class HKDevice: # Ensure the Pairing object has access to the latest version of the entity map. This # is especially important for BLE, as the Pairing instance relies on the entity map # to map aid/iid to GATT characteristics. So push it to there as well. - - self.pairing.pairing_data["accessories"] = self.accessories - self.async_detect_workarounds() # Migrate to new device ids self.async_migrate_devices() - await self.async_load_platforms() - self.async_create_devices() # Load any triggers for this config entry await async_setup_triggers_for_entry(self.hass, self.config_entry) - self.add_entities() - - if self.watchable_characteristics: - await self.pairing.subscribe(self.watchable_characteristics) - if not self.pairing.is_connected: - return - - await self.async_update() - async def async_unload(self) -> None: """Stop interacting with device and prepare for removal from hass.""" if self._polling_interval_remover: @@ -395,26 +438,31 @@ class HKDevice: self.config_entry, self.platforms ) - async def async_refresh_entity_map(self, config_num: int) -> bool: - """Handle setup of a HomeKit accessory.""" - try: - self.accessories = await self.pairing.list_accessories_and_characteristics() - except AccessoryDisconnectedError: - # If we fail to refresh this data then we will naturally retry - # later when Bonjour spots c# is still not up to date. - return False + def async_notify_config_changed(self, config_num: int) -> None: + """Notify the pairing of a config change.""" + self.pairing.notify_config_changed(config_num) - self.entity_map = Accessories.from_list(self.accessories) + def process_config_changed(self, config_num: int) -> None: + """Handle a config change notification from the pairing.""" + self.hass.async_create_task(self.async_update_new_accessories_state()) - self.hass.data[ENTITY_MAP].async_create_or_update_map( - self.unique_id, config_num, self.accessories + async def async_update_new_accessories_state(self) -> None: + """Process a change in the pairings accessories state.""" + self.async_save_entity_map() + await self.async_process_entity_map() + if self.watchable_characteristics: + await self.pairing.subscribe(self.watchable_characteristics) + await self.async_update() + await self.async_add_new_entities() + + @callback + def async_save_entity_map(self) -> None: + """Save the entity map.""" + entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] + entity_storage.async_create_or_update_map( + self.unique_id, self.config_num, self.entity_map.serialize() ) - self.config_num = config_num - self.hass.async_create_task(self.async_process_entity_map()) - - return True - def add_accessory_factory(self, add_entities_cb) -> None: """Add a callback to run when discovering new entities for accessories.""" self.accessory_factories.append(add_entities_cb) @@ -504,10 +552,15 @@ class HKDevice: if tasks: await asyncio.gather(*tasks) + @callback + def async_update_available_state(self, *_: Any) -> None: + """Update the available state of the device.""" + self.async_set_available_state(self.pairing.is_available) + async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" if not self.pollable_characteristics: - self.async_set_available_state(self.pairing.is_connected) + self.async_update_available_state() _LOGGER.debug( "HomeKit connection not polling any characteristics: %s", self.unique_id ) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 6337045a51f..700ab60c47f 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for homekit devices.""" from __future__ import annotations -from collections.abc import Generator +from collections.abc import Callable, Generator from typing import TYPE_CHECKING, Any from aiohomekit.model.characteristics import CharacteristicsTypes @@ -15,6 +15,7 @@ from homeassistant.components.automation import ( AutomationTriggerInfo, ) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers.typing import ConfigType @@ -59,7 +60,9 @@ HK_TO_HA_INPUT_EVENT_VALUES = { class TriggerSource: """Represents a stateless source of event data from HomeKit.""" - def __init__(self, connection, aid, triggers): + def __init__( + self, connection: HKDevice, aid: int, triggers: list[dict[str, Any]] + ) -> None: """Initialize a set of triggers for a device.""" self._hass = connection.hass self._connection = connection @@ -67,7 +70,7 @@ class TriggerSource: self._triggers: dict[tuple[str, str], dict[str, Any]] = {} for trigger in triggers: self._triggers[(trigger["type"], trigger["subtype"])] = trigger - self._callbacks = {} + self._callbacks: dict[int, list[Callable[[Any], None]]] = {} def fire(self, iid, value): """Process events that have been received from a HomeKit accessory.""" @@ -97,7 +100,7 @@ class TriggerSource: trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] iid = trigger["characteristic"] - self._connection.add_watchable_characteristics([(self._aid, iid)]) + await self._connection.add_watchable_characteristics([(self._aid, iid)]) self._callbacks.setdefault(iid, []).append(event_handler) def async_remove_handler(): @@ -193,10 +196,12 @@ TRIGGER_FINDERS = { } -async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): +async def async_setup_triggers_for_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Triggers aren't entities as they have no state, but we still need to set them up for a config entry.""" hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service): diff --git a/homeassistant/components/homekit_controller/diagnostics.py b/homeassistant/components/homekit_controller/diagnostics.py index f83ce7604cf..9b17c0c2fe7 100644 --- a/homeassistant/components/homekit_controller/diagnostics.py +++ b/homeassistant/components/homekit_controller/diagnostics.py @@ -107,6 +107,7 @@ def _async_get_diagnostics( # It is roughly equivalent to what is in .storage/homekit_controller-entity-map # But it also has the latest values seen by the polling or events data["entity-map"] = accessories = connection.entity_map.serialize() + data["config-num"] = connection.config_num # It contains serial numbers, which we should strip out for accessory in accessories: diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 80c2f9870c1..159a1d936fa 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -15,6 +15,10 @@ from homeassistant.components.fan import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from . import KNOWN_DEVICES, HomeKitEntity @@ -48,13 +52,32 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): """Return true if device is on.""" return self.service.value(self.on_characteristic) == 1 + @property + def _speed_range(self) -> tuple[int, int]: + """Return the speed range.""" + return (self._min_speed, self._max_speed) + + @property + def _min_speed(self) -> int: + """Return the minimum speed.""" + return ( + round(self.service[CharacteristicsTypes.ROTATION_SPEED].minValue or 0) + 1 + ) + + @property + def _max_speed(self) -> int: + """Return the minimum speed.""" + return round(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100) + @property def percentage(self) -> int: """Return the current speed percentage.""" if not self.is_on: return 0 - return self.service.value(CharacteristicsTypes.ROTATION_SPEED) + return ranged_value_to_percentage( + self._speed_range, self.service.value(CharacteristicsTypes.ROTATION_SPEED) + ) @property def current_direction(self) -> str: @@ -88,7 +111,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): def speed_count(self) -> int: """Speed count for the fan.""" return round( - min(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100, 100) + min(self._max_speed, 100) / max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0) ) @@ -104,7 +127,11 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): return await self.async_turn_off() await self.async_put_characteristics( - {CharacteristicsTypes.ROTATION_SPEED: percentage} + { + CharacteristicsTypes.ROTATION_SPEED: round( + percentage_to_ranged_value(self._speed_range, percentage) + ) + } ) async def async_oscillate(self, oscillating: bool) -> None: @@ -129,7 +156,9 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): percentage is not None and self.supported_features & FanEntityFeature.SET_SPEED ): - characteristics[CharacteristicsTypes.ROTATION_SPEED] = percentage + characteristics[CharacteristicsTypes.ROTATION_SPEED] = round( + percentage_to_ranged_value(self._speed_range, percentage) + ) if characteristics: await self.async_put_characteristics(characteristics) diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 4073c1ac9fe..df691ac3f6f 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -71,6 +71,18 @@ class HomeKitLight(HomeKitEntity, LightEntity): self.service.value(CharacteristicsTypes.SATURATION), ) + @property + def min_mireds(self) -> int: + """Return minimum supported color temperature.""" + min_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].minValue + return int(min_value) if min_value else super().min_mireds + + @property + def max_mireds(self) -> int: + """Return the maximum color temperature.""" + max_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].maxValue + return int(max_value) if max_value else super().max_mireds + @property def color_temp(self) -> int: """Return the color temperature.""" diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 955f5e37177..ac1be576906 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,9 +3,10 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.20"], - "zeroconf": ["_hap._tcp.local."], - "after_dependencies": ["zeroconf"], + "requirements": ["aiohomekit==1.2.3"], + "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], + "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], + "dependencies": ["bluetooth", "zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"] diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 4e0f5cfa077..7a6d0a01ab6 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -67,8 +67,6 @@ async def async_setup_entry( if description := NUMBER_ENTITIES.get(char.type): entities.append(HomeKitNumber(conn, info, char, description)) - elif entity_type := NUMBER_ENTITY_CLASSES.get(char.type): - entities.append(entity_type(conn, info, char)) else: return False @@ -92,60 +90,16 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): self.entity_description = description super().__init__(conn, info, char) - @property - def name(self) -> str | None: - """Return the name of the device if any.""" - if prefix := super().name: - return f"{prefix} {self.entity_description.name}" - return self.entity_description.name - - def get_characteristic_types(self) -> list[str]: - """Define the homekit characteristics the entity is tracking.""" - return [self._char.type] - - @property - def native_min_value(self) -> float: - """Return the minimum value.""" - return self._char.minValue or DEFAULT_MIN_VALUE - - @property - def native_max_value(self) -> float: - """Return the maximum value.""" - return self._char.maxValue or DEFAULT_MAX_VALUE - - @property - def native_step(self) -> float: - """Return the increment/decrement step.""" - return self._char.minStep or DEFAULT_STEP - - @property - def native_value(self) -> float: - """Return the current characteristic value.""" - return self._char.value - - async def async_set_native_value(self, value: float) -> None: - """Set the characteristic to this value.""" - await self.async_put_characteristics( - { - self._char.type: value, - } - ) - - -class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity): - """Representation of a Number control for Ecobee Fan Mode request.""" - - def get_characteristic_types(self) -> list[str]: - """Define the homekit characteristics the entity is tracking.""" - return [self._char.type] - @property def name(self) -> str: """Return the name of the device if any.""" - prefix = "" - if name := super().name: - prefix = name - return f"{prefix} Fan Mode" + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity is tracking.""" + return [self._char.type] @property def native_min_value(self) -> float: @@ -169,33 +123,8 @@ class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set the characteristic to this value.""" - - # Sending the fan mode request sometimes ends up getting ignored by ecobee - # and this might be because it the older value instead of newer, and ecobee - # thinks there is nothing to do. - # So in order to make sure that the request is executed by ecobee, we need - # to send a different value before sending the target value. - # Fan mode value is a value from 0 to 100. We send a value off by 1 first. - - if value > self.min_value: - other_value = value - 1 - else: - other_value = self.min_value + 1 - - if value != other_value: - await self.async_put_characteristics( - { - self._char.type: other_value, - } - ) - await self.async_put_characteristics( { self._char.type: value, } ) - - -NUMBER_ENTITY_CLASSES: dict[str, type] = { - CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: HomeKitEcobeeFanModeNumber, -} diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index b7d7b8005ed..cddcbc59cde 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -32,8 +32,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity from .connection import HKDevice - -CO2_ICON = "mdi:molecule-co2" +from .utils import folded_name @dataclass @@ -199,7 +198,26 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { } -class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): +class HomeKitSensor(HomeKitEntity, SensorEntity): + """Representation of a HomeKit sensor.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + @property + def name(self) -> str | None: + """Return the name of the device.""" + full_name = super().name + default_name = self.default_name + if ( + default_name + and full_name + and folded_name(default_name) not in folded_name(full_name) + ): + return f"{full_name} {default_name}" + return full_name + + +class HomeKitHumiditySensor(HomeKitSensor): """Representation of a Homekit humidity sensor.""" _attr_device_class = SensorDeviceClass.HUMIDITY @@ -210,9 +228,9 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Humidity" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Humidity" @property def native_value(self) -> float: @@ -220,7 +238,7 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) -class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): +class HomeKitTemperatureSensor(HomeKitSensor): """Representation of a Homekit temperature sensor.""" _attr_device_class = SensorDeviceClass.TEMPERATURE @@ -231,9 +249,9 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.TEMPERATURE_CURRENT] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Temperature" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Temperature" @property def native_value(self) -> float: @@ -241,7 +259,7 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) -class HomeKitLightSensor(HomeKitEntity, SensorEntity): +class HomeKitLightSensor(HomeKitSensor): """Representation of a Homekit light level sensor.""" _attr_device_class = SensorDeviceClass.ILLUMINANCE @@ -252,9 +270,9 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Light Level" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Light Level" @property def native_value(self) -> int: @@ -262,10 +280,10 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT) -class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): +class HomeKitCarbonDioxideSensor(HomeKitSensor): """Representation of a Homekit Carbon Dioxide sensor.""" - _attr_icon = CO2_ICON + _attr_device_class = SensorDeviceClass.CO2 _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION def get_characteristic_types(self) -> list[str]: @@ -273,9 +291,9 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} CO2" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Carbon Dioxide" @property def native_value(self) -> int: @@ -283,7 +301,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL) -class HomeKitBatterySensor(HomeKitEntity, SensorEntity): +class HomeKitBatterySensor(HomeKitSensor): """Representation of a Homekit battery sensor.""" _attr_device_class = SensorDeviceClass.BATTERY @@ -298,9 +316,9 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): ] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Battery" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Battery" @property def icon(self) -> str: @@ -374,7 +392,9 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): @property def name(self) -> str: """Return the name of the device if any.""" - return f"{super().name} {self.entity_description.name}" + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" @property def native_value(self) -> str | int | float: diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 9372764a88a..8c0628c97f6 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -2,12 +2,12 @@ from __future__ import annotations -from typing import Any, TypedDict, cast +from typing import Any, TypedDict from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store -from .const import DOMAIN +from .const import DOMAIN, ENTITY_MAP ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map" ENTITY_MAP_STORAGE_VERSION = 1 @@ -46,7 +46,9 @@ class EntityMapStorage: def __init__(self, hass: HomeAssistant) -> None: """Create a new entity map store.""" self.hass = hass - self.store = Store(hass, ENTITY_MAP_STORAGE_VERSION, ENTITY_MAP_STORAGE_KEY) + self.store = Store[StorageLayout]( + hass, ENTITY_MAP_STORAGE_VERSION, ENTITY_MAP_STORAGE_KEY + ) self.storage_data: dict[str, Pairing] = {} async def async_initialize(self) -> None: @@ -55,8 +57,7 @@ class EntityMapStorage: # There is no cached data about HomeKit devices yet return - storage = cast(StorageLayout, raw_storage) - self.storage_data = storage.get("pairings", {}) + self.storage_data = raw_storage.get("pairings", {}) def get_map(self, homekit_id: str) -> Pairing | None: """Get a pairing cache item.""" @@ -87,6 +88,16 @@ class EntityMapStorage: self.store.async_delay_save(self._data_to_save, ENTITY_MAP_SAVE_DELAY) @callback - def _data_to_save(self) -> dict[str, Any]: + def _data_to_save(self) -> StorageLayout: """Return data of entity map to store in a file.""" - return {"pairings": self.storage_data} + return StorageLayout(pairings=self.storage_data) + + +async def async_get_entity_storage(hass: HomeAssistant) -> EntityMapStorage: + """Get entity storage.""" + if ENTITY_MAP in hass.data: + map_storage: EntityMapStorage = hass.data[ENTITY_MAP] + return map_storage + map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) + await map_storage.async_initialize() + return map_storage diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 7ad868db3fc..2831dabc38d 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -1,7 +1,7 @@ { "title": "HomeKit Controller", "config": { - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "user": { "title": "Device selection", @@ -12,7 +12,7 @@ }, "pair": { "title": "Pair with a device via HomeKit Accessory Protocol", - "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", + "description": "HomeKit Controller communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", "data": { "pairing_code": "Pairing Code", "allow_insecure_setup_codes": "Allow pairing with insecure setup codes." diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 07d0e21e59f..53b3958ecf6 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -147,11 +147,11 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity): super().__init__(conn, info, char) @property - def name(self) -> str | None: + def name(self) -> str: """Return the name of the device if any.""" - if prefix := super().name: - return f"{prefix} {self.entity_description.name}" - return self.entity_description.name + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index ff9f180c943..5cb98f7287d 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -18,7 +18,7 @@ "unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.", "unknown_error": "El dispositiu ha em\u00e8s un error desconegut. Vinculaci\u00f3 fallida." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Atura la vinculaci\u00f3 a tots els controladors o prova de reiniciar el dispositiu, despr\u00e9s, segueix amb la vinculaci\u00f3.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Permet la vinculaci\u00f3 amb codis de configuraci\u00f3 insegurs.", "pairing_code": "Codi de vinculaci\u00f3" }, - "description": "El controlador HomeKit es comunica amb {name} a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura encriptada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el propi dispositiu o en la seva caixa.", + "description": "El controlador HomeKit es comunica amb {name} ({category}) a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura xifrada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el mateix dispositiu o en la seva caixa.", "title": "Vinculaci\u00f3 amb un dispositiu a trav\u00e9s de HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 248d3871b3e..2b61efe6892 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -18,7 +18,7 @@ "unable_to_pair": "Koppeln fehltgeschlagen, bitte versuche es erneut", "unknown_error": "Das Ger\u00e4t meldete einen unbekannten Fehler. Die Kopplung ist fehlgeschlagen." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Breche das Pairing auf allen Controllern ab oder versuche, das Ger\u00e4t neu zu starten, und fahre dann fort, das Pairing fortzusetzen.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Pairing mit unsicheren Setup-Codes zulassen.", "pairing_code": "Kopplungscode" }, - "description": "HomeKit Controller kommuniziert mit {name} \u00fcber das lokale Netzwerk mit einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Gib deinen HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich in der Regel auf dem Ger\u00e4t selbst oder in der Verpackung.", + "description": "HomeKit Controller kommuniziert mit {name} ( {category} ) \u00fcber das lokale Netzwerk unter Verwendung einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Gib deinen HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich normalerweise auf dem Ger\u00e4t selbst oder in der Verpackung.", "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 5de3a6c5334..2686e71d252 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -18,7 +18,7 @@ "unable_to_pair": "Unable to pair, please try again.", "unknown_error": "Device reported an unknown error. Pairing failed." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Abort pairing on all controllers, or try restarting the device, then continue to resume pairing.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Allow pairing with insecure setup codes.", "pairing_code": "Pairing Code" }, - "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", + "description": "HomeKit Controller communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", "title": "Pair with a device via HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json index c658213eea0..8db1df29803 100644 --- a/homeassistant/components/homekit_controller/translations/et.json +++ b/homeassistant/components/homekit_controller/translations/et.json @@ -18,7 +18,7 @@ "unable_to_pair": "Ei saa siduda, proovi uuesti.", "unknown_error": "Seade teatas tundmatust t\u00f5rkest. Sidumine nurjus." }, - "flow_title": "{name}", + "flow_title": "{name} ( {category} )", "step": { "busy_error": { "description": "Katkesta sidumine k\u00f5igis kontrollerites v\u00f5i proovi seade taask\u00e4ivitada ja j\u00e4tka sidumist.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Luba sidumist ebaturvalise salas\u00f5naga.", "pairing_code": "Sidumiskood" }, - "description": "HomeKiti kontroller suhtleb seadmega {name} kohtv\u00f5rgu kaudu, kasutades turvalist kr\u00fcpteeritud \u00fchendust ilma eraldi HomeKiti kontrolleri v\u00f5i iCloudita. Selle lisaseadme kasutamiseks sisesta oma HomeKiti sidumiskood (vormingus XXX-XX-XXX). See kood on tavaliselt seadmel v\u00f5i pakendil.", + "description": "HomeKiti kontroller suhtleb seadmega {name}({category}) kohtv\u00f5rgu kaudu, kasutades turvalist kr\u00fcpteeritud \u00fchendust ilma eraldi HomeKiti kontrolleri v\u00f5i iCloudita. Selle lisaseadme kasutamiseks sisesta oma HomeKiti sidumiskood (vormingus XXX-XX-XXX). See kood on tavaliselt seadmel v\u00f5i pakendil.", "title": "Seadme sidumine HomeKiti tarvikuprotokolli kaudu" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 01286ceb991..b46069de2b8 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -18,7 +18,7 @@ "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", "unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Annulez l'association sur tous les contr\u00f4leurs ou essayez de red\u00e9marrer l'appareil, puis continuez \u00e0 reprendre l'association.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Autoriser le jumelage avec des codes de configuration non s\u00e9curis\u00e9s.", "pairing_code": "Code d\u2019appairage" }, - "description": "Le contr\u00f4leur HomeKit communique avec {name} sur le r\u00e9seau local en utilisant une connexion crypt\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ou iCloud. Entrez votre code d'appariement HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.", + "description": "Le contr\u00f4leur HomeKit communique avec {name} ({category}) sur le r\u00e9seau local en utilisant une connexion chiffr\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ni iCloud. Saisissez votre code d'appairage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.", "title": "Couplage avec un appareil via le protocole accessoire HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index 607c46ede6d..c8f86e46f38 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -11,7 +11,7 @@ "no_devices": "Nem tal\u00e1lhat\u00f3 nem p\u00e1ros\u00edtott eszk\u00f6z" }, "error": { - "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rj\u00fck, ellen\u0151rizze, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rem, ellen\u0151rizze, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", "insecure_setup_code": "A k\u00e9rt telep\u00edt\u00e9si k\u00f3d trivi\u00e1lis jellege miatt nem biztons\u00e1gos. Ez a tartoz\u00e9k nem felel meg az alapvet\u0151 biztons\u00e1gi k\u00f6vetelm\u00e9nyeknek.", "max_peers_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel nincs szabad p\u00e1ros\u00edt\u00e1si t\u00e1rhelye.", "pairing_failed": "Nem kezelt hiba t\u00f6rt\u00e9nt az eszk\u00f6zzel val\u00f3 p\u00e1ros\u00edt\u00e1s sor\u00e1n. Lehet, hogy ez \u00e1tmeneti hiba, vagy az eszk\u00f6z jelenleg m\u00e9g nem t\u00e1mogatott.", diff --git a/homeassistant/components/homekit_controller/translations/id.json b/homeassistant/components/homekit_controller/translations/id.json index 57754bea50b..0514fcca4ed 100644 --- a/homeassistant/components/homekit_controller/translations/id.json +++ b/homeassistant/components/homekit_controller/translations/id.json @@ -18,7 +18,7 @@ "unable_to_pair": "Gagal memasangkan, coba lagi.", "unknown_error": "Perangkat melaporkan kesalahan yang tidak diketahui. Pemasangan gagal." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Batalkan pemasangan di semua pengontrol, atau coba mulai ulang perangkat, lalu lanjutkan untuk melanjutkan pemasangan.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Izinkan pemasangan dengan kode penyiapan yang tidak aman.", "pairing_code": "Kode Pemasangan" }, - "description": "Pengontrol HomeKit berkomunikasi dengan {name} melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Masukkan kode pemasangan HomeKit Anda (dalam format XXX-XX-XXX) untuk menggunakan aksesori ini. Kode ini biasanya ditemukan pada perangkat itu sendiri atau dalam kemasan.", + "description": "Pengontrol HomeKit berkomunikasi dengan {name} ({category}) melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Masukkan kode pemasangan HomeKit Anda (dalam format XXX-XX-XXX) untuk menggunakan aksesori ini. Kode ini biasanya ditemukan pada perangkat itu sendiri atau dalam kemasan.", "title": "Pasangkan dengan perangkat melalui HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index d95eff05cea..5c71b00a801 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -18,7 +18,7 @@ "unable_to_pair": "Impossibile abbinare, riprova.", "unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Interrompi l'associazione su tutti i controller o provare a riavviare il dispositivo, quindi continua a riprendere l'associazione.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Consenti l'associazione con codici di installazione non sicuri.", "pairing_code": "Codice di abbinamento" }, - "description": "Il controller HomeKit comunica con {name} sulla rete locale utilizzando una connessione cifrata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", + "description": "Il controller HomeKit comunica con {name} ({category}) sulla rete locale utilizzando una connessione cifrata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", "title": "Associazione con un dispositivo tramite il Protocollo degli Accessori HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index 8e1e35c60e2..fbac8823897 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -5,7 +5,7 @@ "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "ignored_model": "\u3053\u306e\u30e2\u30c7\u30eb\u306eHomeKit\u3067\u306e\u5bfe\u5fdc\u306f\u3001\u3088\u308a\u5b8c\u5168\u3067\u30cd\u30a4\u30c6\u30a3\u30d6\u306a\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u53ef\u80fd\u306a\u305f\u3081\u3001\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "ignored_model": "\u3053\u306e\u30e2\u30c7\u30eb\u306eHomeKit\u3067\u306e\u5bfe\u5fdc\u306f\u3001\u3088\u308a\u5b8c\u5168\u3067\u30cd\u30a4\u30c6\u30a3\u30d6\u306a\u7d71\u5408\u3092\u4f7f\u7528\u53ef\u80fd\u306a\u305f\u3081\u3001\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "invalid_config_entry": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u6e96\u5099\u304c\u3067\u304d\u3066\u3044\u308b\u3068\u8868\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant\u306b\u306f\u3059\u3067\u306b\u7af6\u5408\u3059\u308b\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308b\u305f\u3081\u3001\u5148\u306b\u3053\u308c\u3092\u524a\u9664\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "invalid_properties": "\u7121\u52b9\u306a\u30d7\u30ed\u30d1\u30c6\u30a3\u304c\u30c7\u30d0\u30a4\u30b9\u306b\u3088\u3063\u3066\u77e5\u3089\u3055\u308c\u307e\u3057\u305f\u3002", "no_devices": "\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index d7b5cc69cf3..d7aba3a17c7 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -18,7 +18,7 @@ "unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie", "unknown_error": "Urz\u0105dzenie zg\u0142osi\u0142o nieznany b\u0142\u0105d. Parowanie nie powiod\u0142o si\u0119." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Przerwij parowanie we wszystkich kontrolerach lub spr\u00f3buj ponownie uruchomi\u0107 urz\u0105dzenie, a nast\u0119pnie wzn\u00f3w parowanie", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Zezwalaj na parowanie z niezabezpieczonymi kodami konfiguracji.", "pairing_code": "Kod parowania" }, - "description": "Kontroler HomeKit komunikuje si\u0119 z {name} poprzez sie\u0107 lokaln\u0105 za pomoc\u0105 bezpiecznego, szyfrowanego po\u0142\u0105czenia bez oddzielnego kontrolera HomeKit lub iCloud. Wprowad\u017a kod parowania (w formacie XXX-XX-XXX), aby u\u017cy\u0107 tego akcesorium. Ten kod zazwyczaj znajduje si\u0119 na samym urz\u0105dzeniu lub w jego opakowaniu.", + "description": "Kontroler HomeKit komunikuje si\u0119 z {name} ({category}) poprzez sie\u0107 lokaln\u0105 za pomoc\u0105 bezpiecznego, szyfrowanego po\u0142\u0105czenia bez oddzielnego kontrolera HomeKit lub iCloud. Wprowad\u017a kod parowania (w formacie XXX-XX-XXX), aby u\u017cy\u0107 tego akcesorium. Ten kod zazwyczaj znajduje si\u0119 na samym urz\u0105dzeniu lub w jego opakowaniu.", "title": "Sparuj z urz\u0105dzeniem poprzez akcesorium HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/pt-BR.json b/homeassistant/components/homekit_controller/translations/pt-BR.json index 67b89e51b08..f84ff6bac37 100644 --- a/homeassistant/components/homekit_controller/translations/pt-BR.json +++ b/homeassistant/components/homekit_controller/translations/pt-BR.json @@ -18,7 +18,7 @@ "unable_to_pair": "N\u00e3o \u00e9 poss\u00edvel parear, tente novamente.", "unknown_error": "O dispositivo relatou um erro desconhecido. O pareamento falhou." }, - "flow_title": "{name}", + "flow_title": "{name} ( {category} )", "step": { "busy_error": { "description": "Abortar o emparelhamento em todos os controladores, ou tentar reiniciar o dispositivo, em seguida, continuar a retomar o emparelhamento.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Permitir o emparelhamento com c\u00f3digos de configura\u00e7\u00e3o inseguros.", "pairing_code": "C\u00f3digo de pareamento" }, - "description": "O HomeKit Controller se comunica com {name} sobre a rede local usando uma conex\u00e3o criptografada segura sem um controlador HomeKit separado ou iCloud. Digite seu c\u00f3digo de emparelhamento HomeKit (no formato XXX-XX-XXX) para usar este acess\u00f3rio. Este c\u00f3digo geralmente \u00e9 encontrado no pr\u00f3prio dispositivo ou na embalagem.", + "description": "O Controlador HomeKit se comunica com {name} ( {category} ) pela rede local usando uma conex\u00e3o criptografada segura sem um controlador HomeKit separado ou iCloud. Insira o c\u00f3digo de pareamento do HomeKit (no formato XXX-XX-XXX) para usar este acess\u00f3rio. Esse c\u00f3digo geralmente \u00e9 encontrado no pr\u00f3prio dispositivo ou na embalagem.", "title": "Emparelhar com um dispositivo atrav\u00e9s do protocolo `HomeKit Accessory`" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/pt.json b/homeassistant/components/homekit_controller/translations/pt.json index c4ab5c1e636..febadec444c 100644 --- a/homeassistant/components/homekit_controller/translations/pt.json +++ b/homeassistant/components/homekit_controller/translations/pt.json @@ -48,7 +48,8 @@ "button6": "Bot\u00e3o 6", "button7": "Bot\u00e3o 7", "button8": "Bot\u00e3o 8", - "button9": "Bot\u00e3o 9" + "button9": "Bot\u00e3o 9", + "doorbell": "Campainha" }, "trigger_type": { "single_press": "\"{subtype}\" pressionado" diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index d4ab6771ee5..00a4d9fcb7c 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -18,7 +18,7 @@ "unable_to_pair": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", "unknown_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u043e\u043e\u0431\u0449\u0438\u043b\u043e \u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435. \u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0432\u0441\u0435\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430\u0445 \u0438\u043b\u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u043c\u0438 \u043a\u043e\u0434\u0430\u043c\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", "pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, - "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441 {name} \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440. \u042d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u043b\u0438 \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0435.", + "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441 {name} ({category}) \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440. \u042d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u043b\u0438 \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0435.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u0432 HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json index 7ddb32ade8e..3086e3e34fb 100644 --- a/homeassistant/components/homekit_controller/translations/tr.json +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -18,7 +18,7 @@ "unable_to_pair": "E\u015fle\u015ftirilemiyor, l\u00fctfen tekrar deneyin.", "unknown_error": "Cihaz bilinmeyen bir hata bildirdi. E\u015fle\u015ftirme ba\u015far\u0131s\u0131z oldu." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "T\u00fcm denetleyicilerde e\u015fle\u015ftirmeyi durdurun veya cihaz\u0131 yeniden ba\u015flatmay\u0131 deneyin, ard\u0131ndan e\u015fle\u015ftirmeye devam edin.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "G\u00fcvenli olmayan kurulum kodlar\u0131yla e\u015fle\u015ftirmeye izin verin.", "pairing_code": "E\u015fle\u015ftirme Kodu" }, - "description": "HomeKit Denetleyici, ayr\u0131 bir HomeKit denetleyicisi veya iCloud olmadan g\u00fcvenli bir \u015fifreli ba\u011flant\u0131 kullanarak yerel alan a\u011f\u0131 \u00fczerinden {name} ile ileti\u015fim kurar. Bu aksesuar\u0131 kullanmak i\u00e7in HomeKit e\u015fle\u015ftirme kodunuzu (XXX-XX-XXX bi\u00e7iminde) girin. Bu kod genellikle cihaz\u0131n kendisinde veya ambalaj\u0131nda bulunur.", + "description": "HomeKit Denetleyici, ayr\u0131 bir HomeKit denetleyicisi veya iCloud olmadan g\u00fcvenli bir \u015fifreli ba\u011flant\u0131 kullanarak yerel alan a\u011f\u0131 \u00fczerinden {name} ( {category} ) ile ileti\u015fim kurar. Bu aksesuar\u0131 kullanmak i\u00e7in HomeKit e\u015fle\u015ftirme kodunuzu (XXX-XX-XXX bi\u00e7iminde) girin. Bu kod genellikle cihaz\u0131n kendisinde veya ambalaj\u0131nda bulunur.", "title": "HomeKit Aksesuar Protokol\u00fc arac\u0131l\u0131\u011f\u0131yla bir cihazla e\u015fle\u015ftirin" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index e13b69bd0fd..63b4b45f520 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -18,7 +18,7 @@ "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "unknown_error": "\u88dd\u7f6e\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "\u53d6\u6d88\u6240\u6709\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u6216\u8005\u91cd\u555f\u88dd\u7f6e\u3001\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "\u5141\u8a31\u8207\u4e0d\u5b89\u5168\u8a2d\u5b9a\u4ee3\u78bc\u9032\u884c\u914d\u5c0d\u3002", "pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc" }, - "description": "\u4f7f\u7528 {name} \u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u88dd\u7f6e\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", + "description": "\u4f7f\u7528 {name} ({category}) \u4e4b HomeKit \u63a7\u5236\u5668\u672c\u5730\u7aef\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u88dd\u7f6e\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", "title": "\u900f\u904e HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a\u6240\u914d\u5c0d\u88dd\u7f6e" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 6831c3cee4a..6e272067b54 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -3,13 +3,18 @@ from typing import cast from aiohomekit import Controller -from homeassistant.components import zeroconf +from homeassistant.components import bluetooth, zeroconf from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from .const import CONTROLLER +def folded_name(name: str) -> str: + """Return a name that is used for matching a similar string.""" + return name.casefold().replace(" ", "") + + async def async_get_controller(hass: HomeAssistant) -> Controller: """Get or create an aiohomekit Controller instance.""" if existing := hass.data.get(CONTROLLER): @@ -23,7 +28,12 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: if existing := hass.data.get(CONTROLLER): return cast(Controller, existing) - controller = Controller(async_zeroconf_instance=async_zeroconf_instance) + bleak_scanner_instance = bluetooth.async_get_scanner(hass) + + controller = Controller( + async_zeroconf_instance=async_zeroconf_instance, + bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type] + ) hass.data[CONTROLLER] = controller diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index f6fb9f3e739..0d06d595f1b 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.4"], + "requirements": ["homematicip==1.0.7"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 80cdbd351b1..57a8b7bd714 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -17,6 +17,7 @@ from homematicip.aio.device import ( AsyncPlugableSwitchMeasuring, AsyncPresenceDetectorIndoor, AsyncRoomControlDeviceAnalog, + AsyncTemperatureDifferenceSensor2, AsyncTemperatureHumiditySensorDisplay, AsyncTemperatureHumiditySensorOutdoor, AsyncTemperatureHumiditySensorWithoutDisplay, @@ -124,6 +125,10 @@ async def async_setup_entry( entities.append(HomematicipTodayRainSensor(hap, device)) if isinstance(device, AsyncPassageDetector): entities.append(HomematicipPassageDetectorDeltaCounter(hap, device)) + if isinstance(device, AsyncTemperatureDifferenceSensor2): + entities.append(HomematicpTemperatureExternalSensorCh1(hap, device)) + entities.append(HomematicpTemperatureExternalSensorCh2(hap, device)) + entities.append(HomematicpTemperatureExternalSensorDelta(hap, device)) if entities: async_add_entities(entities) @@ -387,6 +392,81 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity): return LENGTH_MILLIMETERS +class HomematicpTemperatureExternalSensorCh1(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP device HmIP-STE2-PCB.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Channel 1 Temperature") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_value(self) -> float: + """Return the state.""" + return self._device.temperatureExternalOne + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + +class HomematicpTemperatureExternalSensorCh2(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP device HmIP-STE2-PCB.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Channel 2 Temperature") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_value(self) -> float: + """Return the state.""" + return self._device.temperatureExternalTwo + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + +class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP device HmIP-STE2-PCB.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Delta Temperature") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_value(self) -> float: + """Return the state.""" + return self._device.temperatureExternalDelta + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP passage detector delta counter.""" diff --git a/homeassistant/components/homematicip_cloud/translations/pt.json b/homeassistant/components/homematicip_cloud/translations/pt.json index 645ba242561..2016dc9e13c 100644 --- a/homeassistant/components/homematicip_cloud/translations/pt.json +++ b/homeassistant/components/homematicip_cloud/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O ponto de acesso j\u00e1 se encontra configurado", + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "connection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP", "unknown": "Ocorreu um erro desconhecido." }, diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index 4a087988b61..ec43cdfdd2e 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -75,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index e426234b449..97b3c80b50d 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/homewizard", "codeowners": ["@DCSBL"], "dependencies": [], - "requirements": ["python-homewizard-energy==1.0.3"], + "requirements": ["python-homewizard-energy==1.1.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index b7f759f6a0e..a66a2664ae1 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -31,25 +31,25 @@ _LOGGER = logging.getLogger(__name__) SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( key="smr_version", - name="DSMR Version", + name="DSMR version", icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="meter_model", - name="Smart Meter Model", + name="Smart meter model", icon="mdi:gauge", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="wifi_ssid", - name="Wifi SSID", + name="Wi-Fi SSID", icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="wifi_strength", - name="Wifi Strength", + name="Wi-Fi strength", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -58,67 +58,81 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( ), SensorEntityDescription( key="total_power_import_t1_kwh", - name="Total Power Import T1", + name="Total power import T1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="total_power_import_t2_kwh", - name="Total Power Import T2", + name="Total power import T2", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="total_power_export_t1_kwh", - name="Total Power Export T1", + name="Total power export T1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="total_power_export_t2_kwh", - name="Total Power Export T2", + name="Total power export T2", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="active_power_w", - name="Active Power", + name="Active power", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="active_power_l1_w", - name="Active Power L1", + name="Active power L1", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="active_power_l2_w", - name="Active Power L2", + name="Active power L2", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="active_power_l3_w", - name="Active Power L3", + name="Active power L3", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="total_gas_m3", - name="Total Gas", + name="Total gas", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="active_liter_lpm", + name="Active water usage", + native_unit_of_measurement="l/min", + icon="mdi:water", + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="total_liter_m3", + name="Total water usage", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + icon="mdi:gauge", + state_class=SensorStateClass.TOTAL_INCREASING, + ), ) @@ -139,6 +153,8 @@ async def async_setup_entry( class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorEntity): """Representation of a HomeWizard Sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -152,7 +168,6 @@ class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorE self.entry = entry # Config attributes. - self._attr_name = f"{entry.title} {description.name}" self.data_type = description.key self._attr_unique_id = f"{entry.unique_id}_{description.key}" diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index eb2e9c49afe..eca8a7670be 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -36,6 +36,8 @@ class HWEnergySwitchEntity( ): """Representation switchable entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -65,9 +67,6 @@ class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): """Initialize the switch.""" super().__init__(coordinator, entry, "power_on") - # Config attributes - self._attr_name = f"{entry.title} Switch" - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.coordinator.api.state_set(power_on=True) @@ -101,6 +100,7 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): It disables any method that can turn of the relay. """ + _attr_name = "Switch lock" _attr_device_class = SwitchDeviceClass.SWITCH _attr_entity_category = EntityCategory.CONFIG @@ -110,9 +110,6 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): """Initialize the switch.""" super().__init__(coordinator, entry, "switch_lock") - # Config attributes - self._attr_name = f"{entry.title} Switch Lock" - async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch-lock on.""" await self.coordinator.api.state_set(switch_lock=True) diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index bad0ed96e01..146b8e3c035 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -77,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await data.async_update() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = data - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/honeywell/translations/hu.json b/homeassistant/components/honeywell/translations/hu.json index 14ba9167ea5..b0552b23fe1 100644 --- a/homeassistant/components/honeywell/translations/hu.json +++ b/homeassistant/components/honeywell/translations/hu.json @@ -9,7 +9,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat." + "description": "K\u00e9rem, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat." } } }, diff --git a/homeassistant/components/honeywell/translations/pt.json b/homeassistant/components/honeywell/translations/pt.json new file mode 100644 index 00000000000..ebb712d0682 --- /dev/null +++ b/homeassistant/components/honeywell/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 374e69975ce..7c8594bdd90 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -7,7 +7,7 @@ import logging import os import ssl from tempfile import NamedTemporaryFile -from typing import Any, Final, Optional, TypedDict, Union, cast +from typing import Any, Final, TypedDict, Union, cast from aiohttp import web from aiohttp.typedefs import StrOrURL @@ -125,10 +125,10 @@ class ConfData(TypedDict, total=False): @bind_hass -async def async_get_last_config(hass: HomeAssistant) -> dict | None: +async def async_get_last_config(hass: HomeAssistant) -> dict[str, Any] | None: """Return the last known working config.""" - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) - return cast(Optional[dict], await store.async_load()) + store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + return await store.async_load() class ApiConfig: @@ -475,7 +475,9 @@ async def start_http_server_and_save_config( await server.start() # If we are set up successful, we store the HTTP settings for safe mode. - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + store: storage.Store[dict[str, Any]] = storage.Store( + hass, STORAGE_VERSION, STORAGE_KEY + ) if CONF_TRUSTED_PROXIES in conf: conf[CONF_TRUSTED_PROXIES] = [ diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 09ef6e13e03..7c6f445ce80 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -6,7 +6,7 @@ from datetime import timedelta from ipaddress import ip_address import logging import secrets -from typing import Final +from typing import Any, Final from aiohttp import hdrs from aiohttp.web import Application, Request, StreamResponse, middleware @@ -118,8 +118,8 @@ def async_user_not_allowed_do_auth( async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: """Create auth middleware for the app.""" - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - if (data := await store.async_load()) is None or not isinstance(data, dict): + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + if (data := await store.async_load()) is None: data = {} refresh_token = None @@ -218,7 +218,7 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: # for every request. elif ( request.method == "GET" - and SIGN_QUERY_PARAM in request.query + and SIGN_QUERY_PARAM in request.query_string and await async_validate_signed_request(request) ): authenticated = True diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 620bdc7613c..d2f5f9d8ba5 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -2,17 +2,18 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from contextlib import suppress from datetime import datetime from http import HTTPStatus from ipaddress import IPv4Address, IPv6Address, ip_address import logging from socket import gethostbyaddr, herror -from typing import Any, Final +from typing import Any, Final, TypeVar -from aiohttp.web import Application, Request, StreamResponse, middleware +from aiohttp.web import Application, Request, Response, StreamResponse, middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import persistent_notification @@ -24,9 +25,12 @@ from homeassistant.util import dt as dt_util, yaml from .view import HomeAssistantView +_HassViewT = TypeVar("_HassViewT", bound=HomeAssistantView) +_P = ParamSpec("_P") + _LOGGER: Final = logging.getLogger(__name__) -KEY_BANNED_IPS: Final = "ha_banned_ips" +KEY_BAN_MANAGER: Final = "ha_banned_ips_manager" KEY_FAILED_LOGIN_ATTEMPTS: Final = "ha_failed_login_attempts" KEY_LOGIN_THRESHOLD: Final = "ha_login_threshold" @@ -50,9 +54,9 @@ def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> N async def ban_startup(app: Application) -> None: """Initialize bans when app starts up.""" - app[KEY_BANNED_IPS] = await async_load_ip_bans_config( - hass, hass.config.path(IP_BANS_FILE) - ) + ban_manager = IpBanManager(hass) + await ban_manager.async_load() + app[KEY_BAN_MANAGER] = ban_manager app.on_startup.append(ban_startup) @@ -62,18 +66,17 @@ async def ban_middleware( request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] ) -> StreamResponse: """IP Ban middleware.""" - if KEY_BANNED_IPS not in request.app: + ban_manager: IpBanManager | None = request.app.get(KEY_BAN_MANAGER) + if ban_manager is None: _LOGGER.error("IP Ban middleware loaded but banned IPs not loaded") return await handler(request) - # Verify if IP is not banned - ip_address_ = ip_address(request.remote) # type: ignore[arg-type] - is_banned = any( - ip_ban.ip_address == ip_address_ for ip_ban in request.app[KEY_BANNED_IPS] - ) - - if is_banned: - raise HTTPForbidden() + ip_bans_lookup = ban_manager.ip_bans_lookup + if ip_bans_lookup: + # Verify if IP is not banned + ip_address_ = ip_address(request.remote) # type: ignore[arg-type] + if ip_address_ in ip_bans_lookup: + raise HTTPForbidden() try: return await handler(request) @@ -83,13 +86,13 @@ async def ban_middleware( def log_invalid_auth( - func: Callable[..., Awaitable[StreamResponse]] -) -> Callable[..., Awaitable[StreamResponse]]: + func: Callable[Concatenate[_HassViewT, Request, _P], Awaitable[Response]] +) -> Callable[Concatenate[_HassViewT, Request, _P], Coroutine[Any, Any, Response]]: """Decorate function to handle invalid auth or failed login attempts.""" async def handle_req( - view: HomeAssistantView, request: Request, *args: Any, **kwargs: Any - ) -> StreamResponse: + view: _HassViewT, request: Request, *args: _P.args, **kwargs: _P.kwargs + ) -> Response: """Try to log failed login attempts if response status >= BAD_REQUEST.""" resp = await func(view, request, *args, **kwargs) if resp.status >= HTTPStatus.BAD_REQUEST: @@ -118,7 +121,7 @@ async def process_wrong_login(request: Request) -> None: # The user-agent is unsanitized input so we only include it in the log user_agent = request.headers.get("user-agent") - log_msg = f"{base_msg} ({user_agent})" + log_msg = f"{base_msg} Requested URL: '{request.rel_url}'. ({user_agent})" notification_msg = f"{base_msg} See the log for details." @@ -129,7 +132,7 @@ async def process_wrong_login(request: Request) -> None: ) # Check if ban middleware is loaded - if KEY_BANNED_IPS not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: + if KEY_BAN_MANAGER not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: return request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1 @@ -146,14 +149,9 @@ async def process_wrong_login(request: Request) -> None: request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >= request.app[KEY_LOGIN_THRESHOLD] ): - new_ban = IpBan(remote_addr) - request.app[KEY_BANNED_IPS].append(new_ban) - - await hass.async_add_executor_job( - update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban - ) - + ban_manager: IpBanManager = request.app[KEY_BAN_MANAGER] _LOGGER.warning("Banned IP %s for too many login attempts", remote_addr) + await ban_manager.async_add_ban(remote_addr) persistent_notification.async_create( hass, @@ -173,7 +171,7 @@ async def process_success_login(request: Request) -> None: remote_addr = ip_address(request.remote) # type: ignore[arg-type] # Check if ban middleware is loaded - if KEY_BANNED_IPS not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: + if KEY_BAN_MANAGER not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: return if ( @@ -199,32 +197,49 @@ class IpBan: self.banned_at = banned_at or dt_util.utcnow() -async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> list[IpBan]: - """Load list of banned IPs from config file.""" - ip_list: list[IpBan] = [] +class IpBanManager: + """Manage IP bans.""" - try: - list_ = await hass.async_add_executor_job(load_yaml_config_file, path) - except FileNotFoundError: - return ip_list - except HomeAssistantError as err: - _LOGGER.error("Unable to load %s: %s", path, str(err)) - return ip_list + def __init__(self, hass: HomeAssistant) -> None: + """Init the ban manager.""" + self.hass = hass + self.path = hass.config.path(IP_BANS_FILE) + self.ip_bans_lookup: dict[IPv4Address | IPv6Address, IpBan] = {} - for ip_ban, ip_info in list_.items(): + async def async_load(self) -> None: + """Load the existing IP bans.""" try: - ip_info = SCHEMA_IP_BAN_ENTRY(ip_info) - ip_list.append(IpBan(ip_ban, ip_info["banned_at"])) - except vol.Invalid as err: - _LOGGER.error("Failed to load IP ban %s: %s", ip_info, err) - continue + list_ = await self.hass.async_add_executor_job( + load_yaml_config_file, self.path + ) + except FileNotFoundError: + return + except HomeAssistantError as err: + _LOGGER.error("Unable to load %s: %s", self.path, str(err)) + return - return ip_list + ip_bans_lookup: dict[IPv4Address | IPv6Address, IpBan] = {} + for ip_ban, ip_info in list_.items(): + try: + ip_info = SCHEMA_IP_BAN_ENTRY(ip_info) + ban = IpBan(ip_ban, ip_info["banned_at"]) + ip_bans_lookup[ban.ip_address] = ban + except vol.Invalid as err: + _LOGGER.error("Failed to load IP ban %s: %s", ip_info, err) + continue + self.ip_bans_lookup = ip_bans_lookup -def update_ip_bans_config(path: str, ip_ban: IpBan) -> None: - """Update config file with new banned IP address.""" - with open(path, "a", encoding="utf8") as out: - ip_ = {str(ip_ban.ip_address): {ATTR_BANNED_AT: ip_ban.banned_at.isoformat()}} - out.write("\n") - out.write(yaml.dump(ip_)) + def _add_ban(self, ip_ban: IpBan) -> None: + """Update config file with new banned IP address.""" + with open(self.path, "a", encoding="utf8") as out: + ip_ = { + str(ip_ban.ip_address): {ATTR_BANNED_AT: ip_ban.banned_at.isoformat()} + } + # Write in a single write call to avoid interleaved writes + out.write("\n" + yaml.dump(ip_)) + + async def async_add_ban(self, remote_addr: IPv4Address | IPv6Address) -> None: + """Add a new IP address to the banned list.""" + new_ban = self.ip_bans_lookup[remote_addr] = IpBan(remote_addr) + await self.hass.async_add_executor_job(self._add_ban, new_ban) diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index cc661d43fd8..6647a6436c5 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -1,17 +1,21 @@ """Decorator for view methods to help with data validation.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from http import HTTPStatus import logging -from typing import Any +from typing import Any, TypeVar from aiohttp import web +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from .view import HomeAssistantView +_HassViewT = TypeVar("_HassViewT", bound=HomeAssistantView) +_P = ParamSpec("_P") + _LOGGER = logging.getLogger(__name__) @@ -33,33 +37,40 @@ class RequestDataValidator: self._allow_empty = allow_empty def __call__( - self, method: Callable[..., Awaitable[web.StreamResponse]] - ) -> Callable: + self, + method: Callable[ + Concatenate[_HassViewT, web.Request, dict[str, Any], _P], + Awaitable[web.Response], + ], + ) -> Callable[ + Concatenate[_HassViewT, web.Request, _P], + Coroutine[Any, Any, web.Response], + ]: """Decorate a function.""" @wraps(method) async def wrapper( - view: HomeAssistantView, request: web.Request, *args: Any, **kwargs: Any - ) -> web.StreamResponse: + view: _HassViewT, request: web.Request, *args: _P.args, **kwargs: _P.kwargs + ) -> web.Response: """Wrap a request handler with data validation.""" - data = None + raw_data = None try: - data = await request.json() + raw_data = await request.json() except ValueError: if not self._allow_empty or (await request.content.read()) != b"": _LOGGER.error("Invalid JSON received") return view.json_message("Invalid JSON.", HTTPStatus.BAD_REQUEST) - data = {} + raw_data = {} try: - kwargs["data"] = self._schema(data) + data: dict[str, Any] = self._schema(raw_data) except vol.Invalid as err: _LOGGER.error("Data does not match schema: %s", err) return view.json_message( f"Message format incorrect: {err}", HTTPStatus.BAD_REQUEST ) - result = await method(view, request, *args, **kwargs) + result = await method(view, request, data, *args, **kwargs) return result return wrapper diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 112549553eb..6cb1bafdaca 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -9,11 +9,31 @@ from aiohttp import hdrs from aiohttp.web import FileResponse, Request, StreamResponse from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound from aiohttp.web_urldispatcher import StaticResource +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant.core import HomeAssistant + +from .const import KEY_HASS CACHE_TIME: Final = 31 * 86400 # = 1 month CACHE_HEADERS: Final[Mapping[str, str]] = { hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}" } +PATH_CACHE = LRU(512) + + +def _get_file_path( + filename: str | Path, directory: Path, follow_symlinks: bool +) -> Path | None: + filepath = directory.joinpath(filename).resolve() + if not follow_symlinks: + filepath.relative_to(directory) + # on opening a dir, load its contents if allowed + if filepath.is_dir(): + return None + if filepath.is_file(): + return filepath + raise FileNotFoundError class CachingStaticResource(StaticResource): @@ -21,16 +41,19 @@ class CachingStaticResource(StaticResource): async def _handle(self, request: Request) -> StreamResponse: rel_url = request.match_info["filename"] + hass: HomeAssistant = request.app[KEY_HASS] + filename = Path(rel_url) + if filename.anchor: + # rel_url is an absolute name like + # /static/\\machine_name\c$ or /static/D:\path + # where the static dir is totally different + raise HTTPForbidden() try: - filename = Path(rel_url) - if filename.anchor: - # rel_url is an absolute name like - # /static/\\machine_name\c$ or /static/D:\path - # where the static dir is totally different - raise HTTPForbidden() - filepath = self._directory.joinpath(filename).resolve() - if not self._follow_symlinks: - filepath.relative_to(self._directory) + key = (filename, self._directory, self._follow_symlinks) + if (filepath := PATH_CACHE.get(key)) is None: + filepath = PATH_CACHE[key] = await hass.async_add_executor_job( + _get_file_path, filename, self._directory, self._follow_symlinks + ) except (ValueError, FileNotFoundError) as error: # relatively safe raise HTTPNotFound() from error @@ -39,13 +62,10 @@ class CachingStaticResource(StaticResource): request.app.logger.exception(error) raise HTTPNotFound() from error - # on opening a dir, load its contents if allowed - if filepath.is_dir(): - return await super()._handle(request) - if filepath.is_file(): + if filepath: return FileResponse( filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS, ) - raise HTTPNotFound + return await super()._handle(request) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index f4e2cb209db..bace633f128 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -74,6 +74,7 @@ from .const import ( KEY_SMS_SMS_COUNT, KEY_WLAN_HOST_LIST, KEY_WLAN_WIFI_FEATURE_SWITCH, + KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, NOTIFY_SUPPRESS_TIMEOUT, SERVICE_CLEAR_TRAFFIC_STATISTICS, SERVICE_REBOOT, @@ -275,6 +276,18 @@ class Router: self._get_data( KEY_WLAN_WIFI_FEATURE_SWITCH, self.client.wlan.wifi_feature_switch ) + self._get_data( + KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, + lambda: next( + filter( + lambda ssid: ssid.get("wifiisguestnetwork") == "1", + self.client.wlan.multi_basic_settings() + .get("Ssids", {}) + .get("Ssid", []), + ), + {}, + ), + ) dispatcher_send(self.hass, UPDATE_SIGNAL, self.config_entry.unique_id) @@ -421,7 +434,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Forward config entry setup to platforms - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Notify doesn't support config entry setup yet, load with discovery for now await discovery.async_load_platform( @@ -573,10 +586,7 @@ class HuaweiLteBaseEntity(Entity): _available: bool = field(default=True, init=False) _unsub_handlers: list[Callable] = field(default_factory=list, init=False) - - @property - def _entity_name(self) -> str: - raise NotImplementedError + _attr_has_entity_name: bool = field(default=True, init=False) @property def _device_unique_id(self) -> str: @@ -588,11 +598,6 @@ class HuaweiLteBaseEntity(Entity): """Return unique ID for entity.""" return f"{self.router.config_entry.unique_id}-{self._device_unique_id}" - @property - def name(self) -> str: - """Return entity name.""" - return f"Huawei {self.router.device_name} {self._entity_name}" - @property def available(self) -> bool: """Return whether the entity is available.""" diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 9bd455d9d59..695b7e2e815 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -105,15 +105,13 @@ CONNECTION_STATE_ATTRIBUTES = { class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): """Huawei LTE mobile connection binary sensor.""" + _attr_name: str = field(default="Mobile connection", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_STATUS self.item = "ConnectionStatus" - @property - def _entity_name(self) -> str: - return "Mobile connection" - @property def is_on(self) -> bool: """Return whether the binary sensor is on.""" @@ -176,57 +174,49 @@ class HuaweiLteBaseWifiStatusBinarySensor(HuaweiLteBaseBinarySensor): class HuaweiLteWifiStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE WiFi status binary sensor.""" + _attr_name: str = field(default="WiFi status", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_STATUS self.item = "WifiStatus" - @property - def _entity_name(self) -> str: - return "WiFi status" - @dataclass class HuaweiLteWifi24ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE 2.4GHz WiFi status binary sensor.""" + _attr_name: str = field(default="2.4GHz WiFi status", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_FEATURE_SWITCH self.item = "wifi24g_switch_enable" - @property - def _entity_name(self) -> str: - return "2.4GHz WiFi status" - @dataclass class HuaweiLteWifi5ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE 5GHz WiFi status binary sensor.""" + _attr_name: str = field(default="5GHz WiFi status", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_FEATURE_SWITCH self.item = "wifi5g_enabled" - @property - def _entity_name(self) -> str: - return "5GHz WiFi status" - @dataclass class HuaweiLteSmsStorageFullBinarySensor(HuaweiLteBaseBinarySensor): """Huawei LTE SMS storage full binary sensor.""" + _attr_name: str = field(default="SMS storage full", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_CHECK_NOTIFICATIONS self.item = "SmsStorageFull" - @property - def _entity_name(self) -> str: - return "SMS storage full" - @property def is_on(self) -> bool: """Return whether the binary sensor is on.""" diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index b9cbf546087..754be6bf2f3 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -43,6 +43,7 @@ KEY_NET_NET_MODE = "net_net_mode" KEY_SMS_SMS_COUNT = "sms_sms_count" KEY_WLAN_HOST_LIST = "wlan_host_list" KEY_WLAN_WIFI_FEATURE_SWITCH = "wlan_wifi_feature_switch" +KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH = "wlan_wifi_guest_network_switch" BINARY_SENSOR_KEYS = { KEY_MONITORING_CHECK_NOTIFICATIONS, @@ -67,7 +68,7 @@ SENSOR_KEYS = { KEY_SMS_SMS_COUNT, } -SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH} +SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH, KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH} ALL_KEYS = ( BINARY_SENSOR_KEYS diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index ee643c59b12..179587d72d7 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -185,7 +185,8 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): _extra_state_attributes: dict[str, Any] = field(default_factory=dict, init=False) @property - def _entity_name(self) -> str: + def name(self) -> str: + """Return the name of the entity.""" return self.hostname or self.mac_address @property diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index c1b15d57620..910e0e132f1 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "huawei-lte-api==1.6.1", "stringcase==1.2.0", - "url-normalize==1.4.1" + "url-normalize==1.4.3" ], "ssdp": [ { diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 85f63ed2bc6..c4cce70cbb7 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -62,9 +62,18 @@ class SensorMeta(NamedTuple): SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { + # + # Device information + # KEY_DEVICE_INFORMATION: SensorMeta( include=re.compile(r"^(WanIP.*Address|uptime)$", re.IGNORECASE) ), + (KEY_DEVICE_INFORMATION, "uptime"): SensorMeta( + name="Uptime", + icon="mdi:timer-outline", + native_unit_of_measurement=TIME_SECONDS, + entity_category=EntityCategory.DIAGNOSTIC, + ), (KEY_DEVICE_INFORMATION, "WanIPAddress"): SensorMeta( name="WAN IP address", icon="mdi:ip", @@ -76,12 +85,9 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_INFORMATION, "uptime"): SensorMeta( - name="Uptime", - icon="mdi:timer-outline", - native_unit_of_measurement=TIME_SECONDS, - entity_category=EntityCategory.DIAGNOSTIC, - ), + # + # Signal + # (KEY_DEVICE_SIGNAL, "band"): SensorMeta( name="Band", entity_category=EntityCategory.DIAGNOSTIC, @@ -91,6 +97,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), + (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( + name="CQI 0", + icon="mdi:speedometer", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( + name="CQI 1", + icon="mdi:speedometer", + ), (KEY_DEVICE_SIGNAL, "dl_mcs"): SensorMeta( name="Downlink MCS", entity_category=EntityCategory.DIAGNOSTIC, @@ -108,49 +123,42 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="EARFCN", entity_category=EntityCategory.DIAGNOSTIC, ), + (KEY_DEVICE_SIGNAL, "ecio"): SensorMeta( + name="EC/IO", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + # https://wiki.teltonika.lt/view/EC/IO + icon=lambda x: ( + "mdi:signal-cellular-outline", + "mdi:signal-cellular-1", + "mdi:signal-cellular-2", + "mdi:signal-cellular-3", + )[bisect((-20, -10, -6), x if x is not None else -1000)], + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "enodeb_id"): SensorMeta( + name="eNodeB ID", + entity_category=EntityCategory.DIAGNOSTIC, + ), (KEY_DEVICE_SIGNAL, "lac"): SensorMeta( name="LAC", icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta( - name="PLMN", + (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( + name="Downlink frequency", + formatter=lambda x: ( + round(int(x) / 10) if x is not None else None, + FREQUENCY_MEGAHERTZ, + ), entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "rac"): SensorMeta( - name="RAC", - icon="mdi:map-marker", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta( - name="RRC status", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "tac"): SensorMeta( - name="TAC", - icon="mdi:map-marker", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta( - name="TDD", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "txpower"): SensorMeta( - name="Transmit power", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta( - name="Uplink MCS", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "ulbandwidth"): SensorMeta( - name="Uplink bandwidth", - icon=lambda x: ( - "mdi:speedometer-slow", - "mdi:speedometer-medium", - "mdi:speedometer", - )[bisect((8, 15), x if x is not None else -1000)], + (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( + name="Uplink frequency", + formatter=lambda x: ( + round(int(x) / 10) if x is not None else None, + FREQUENCY_MEGAHERTZ, + ), entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "mode"): SensorMeta( @@ -168,19 +176,31 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( - name="RSRQ", + (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta( + name="PLMN", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rac"): SensorMeta( + name="RAC", + icon="mdi:map-marker", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta( + name="RRC status", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta( + name="RSCP", device_class=SensorDeviceClass.SIGNAL_STRENGTH, - # http://www.lte-anbieter.info/technik/rsrq.php + # https://wiki.teltonika.lt/view/RSCP icon=lambda x: ( "mdi:signal-cellular-outline", "mdi:signal-cellular-1", "mdi:signal-cellular-2", "mdi:signal-cellular-3", - )[bisect((-11, -8, -5), x if x is not None else -1000)], + )[bisect((-95, -85, -75), x if x is not None else -1000)], state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rsrp"): SensorMeta( name="RSRP", @@ -196,6 +216,20 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), + (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( + name="RSRQ", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + # http://www.lte-anbieter.info/technik/rsrq.php + icon=lambda x: ( + "mdi:signal-cellular-outline", + "mdi:signal-cellular-1", + "mdi:signal-cellular-2", + "mdi:signal-cellular-3", + )[bisect((-11, -8, -5), x if x is not None else -1000)], + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + ), (KEY_DEVICE_SIGNAL, "rssi"): SensorMeta( name="RSSI", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -224,65 +258,40 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), - (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta( - name="RSCP", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - # https://wiki.teltonika.lt/view/RSCP - icon=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-95, -85, -75), x if x is not None else -1000)], - state_class=SensorStateClass.MEASUREMENT, + (KEY_DEVICE_SIGNAL, "tac"): SensorMeta( + name="TAC", + icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "ecio"): SensorMeta( - name="EC/IO", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - # https://wiki.teltonika.lt/view/EC/IO - icon=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-20, -10, -6), x if x is not None else -1000)], - state_class=SensorStateClass.MEASUREMENT, + (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta( + name="TDD", entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "transmode"): SensorMeta( name="Transmission mode", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( - name="CQI 0", - icon="mdi:speedometer", + (KEY_DEVICE_SIGNAL, "txpower"): SensorMeta( + name="Transmit power", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( - name="CQI 1", - icon="mdi:speedometer", - ), - (KEY_DEVICE_SIGNAL, "enodeb_id"): SensorMeta( - name="eNodeB ID", + (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta( + name="Uplink MCS", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( - name="Downlink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - FREQUENCY_MEGAHERTZ, - ), - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( - name="Uplink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - FREQUENCY_MEGAHERTZ, - ), + (KEY_DEVICE_SIGNAL, "ulbandwidth"): SensorMeta( + name="Uplink bandwidth", + icon=lambda x: ( + "mdi:speedometer-slow", + "mdi:speedometer-medium", + "mdi:speedometer", + )[bisect((8, 15), x if x is not None else -1000)], entity_category=EntityCategory.DIAGNOSTIC, ), + # + # Monitoring + # KEY_MONITORING_CHECK_NOTIFICATIONS: SensorMeta( exclude=re.compile( r"^(onlineupdatestatus|smsstoragefull)$", @@ -290,7 +299,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ) ), (KEY_MONITORING_CHECK_NOTIFICATIONS, "UnreadMessage"): SensorMeta( - name="SMS unread", icon="mdi:email-receive" + name="SMS unread", icon="mdi:email-arrow-left" ), KEY_MONITORING_MONTH_STATISTICS: SensorMeta( exclude=re.compile(r"^month(duration|lastcleartime)$", re.IGNORECASE) @@ -331,13 +340,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_MONITORING_STATUS, "SecondaryDns"): SensorMeta( - name="Secondary DNS server", + (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): SensorMeta( + name="Primary IPv6 DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): SensorMeta( - name="Primary IPv6 DNS server", + (KEY_MONITORING_STATUS, "SecondaryDns"): SensorMeta( + name="Secondary DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -396,14 +405,12 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), + # + # Network + # KEY_NET_CURRENT_PLMN: SensorMeta( exclude=re.compile(r"^(Rat|ShortName|Spn)$", re.IGNORECASE) ), - (KEY_NET_CURRENT_PLMN, "State"): SensorMeta( - name="Operator search mode", - formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), - entity_category=EntityCategory.CONFIG, - ), (KEY_NET_CURRENT_PLMN, "FullName"): SensorMeta( name="Operator name", entity_category=EntityCategory.DIAGNOSTIC, @@ -412,6 +419,11 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="Operator code", entity_category=EntityCategory.DIAGNOSTIC, ), + (KEY_NET_CURRENT_PLMN, "State"): SensorMeta( + name="Operator search mode", + formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), + entity_category=EntityCategory.CONFIG, + ), KEY_NET_NET_MODE: SensorMeta(include=re.compile(r"^NetworkMode$", re.IGNORECASE)), (KEY_NET_NET_MODE, "NetworkMode"): SensorMeta( name="Preferred mode", @@ -429,13 +441,16 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), entity_category=EntityCategory.CONFIG, ), + # + # SMS + # (KEY_SMS_SMS_COUNT, "LocalDeleted"): SensorMeta( name="SMS deleted (device)", icon="mdi:email-minus", ), (KEY_SMS_SMS_COUNT, "LocalDraft"): SensorMeta( name="SMS drafts (device)", - icon="mdi:email-send-outline", + icon="mdi:email-arrow-right-outline", ), (KEY_SMS_SMS_COUNT, "LocalInbox"): SensorMeta( name="SMS inbox (device)", @@ -447,15 +462,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_SMS_SMS_COUNT, "LocalOutbox"): SensorMeta( name="SMS outbox (device)", - icon="mdi:email-send", + icon="mdi:email-arrow-right", ), (KEY_SMS_SMS_COUNT, "LocalUnread"): SensorMeta( name="SMS unread (device)", - icon="mdi:email-receive", + icon="mdi:email-arrow-left", ), (KEY_SMS_SMS_COUNT, "SimDraft"): SensorMeta( name="SMS drafts (SIM)", - icon="mdi:email-send-outline", + icon="mdi:email-arrow-right-outline", ), (KEY_SMS_SMS_COUNT, "SimInbox"): SensorMeta( name="SMS inbox (SIM)", @@ -467,15 +482,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_SMS_SMS_COUNT, "SimOutbox"): SensorMeta( name="SMS outbox (SIM)", - icon="mdi:email-send", + icon="mdi:email-arrow-right", ), (KEY_SMS_SMS_COUNT, "SimUnread"): SensorMeta( name="SMS unread (SIM)", - icon="mdi:email-receive", + icon="mdi:email-arrow-left", ), (KEY_SMS_SMS_COUNT, "SimUsed"): SensorMeta( name="SMS messages (SIM)", - icon="mdi:email-receive", + icon="mdi:email-arrow-left", ), } @@ -533,6 +548,10 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): _state: StateType = field(default=STATE_UNKNOWN, init=False) _unit: str | None = field(default=None, init=False) + def __post_init__(self) -> None: + """Initialize remaining attributes.""" + self._attr_name = self.meta.name or self.item + async def async_added_to_hass(self) -> None: """Subscribe to needed data on add.""" await super().async_added_to_hass() @@ -543,10 +562,6 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): await super().async_will_remove_from_hass() self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}") - @property - def _entity_name(self) -> str: - return self.meta.name or self.item - @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index cc5e8e446c5..78579d62698 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -16,7 +16,11 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HuaweiLteBaseEntityWithDevice -from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH +from .const import ( + DOMAIN, + KEY_DIALUP_MOBILE_DATASWITCH, + KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, +) _LOGGER = logging.getLogger(__name__) @@ -33,6 +37,9 @@ async def async_setup_entry( if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): switches.append(HuaweiLteMobileDataSwitch(router)) + if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH).get("WifiEnable"): + switches.append(HuaweiLteWifiGuestNetworkSwitch(router)) + async_add_entities(switches, True) @@ -85,15 +92,13 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntityWithDevice, SwitchEntity): class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): """Huawei LTE mobile data switch device.""" + _attr_name: str = field(default="Mobile data", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_DIALUP_MOBILE_DATASWITCH self.item = "dataswitch" - @property - def _entity_name(self) -> str: - return "Mobile data" - @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" @@ -113,3 +118,39 @@ class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): def icon(self) -> str: """Return switch icon.""" return "mdi:signal" if self.is_on else "mdi:signal-off" + + +@dataclass +class HuaweiLteWifiGuestNetworkSwitch(HuaweiLteBaseSwitch): + """Huawei LTE WiFi guest network switch device.""" + + _attr_name: str = field(default="WiFi guest network", init=False) + + def __post_init__(self) -> None: + """Initialize identifiers.""" + self.key = KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH + self.item = "WifiEnable" + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + + @property + def is_on(self) -> bool: + """Return whether the switch is on.""" + return self._raw_state == "1" + + def _turn(self, state: bool) -> None: + self.router.client.wlan.wifi_guest_network_switch(state) + self._raw_state = "1" if state else "0" + self.schedule_update_ha_state() + + @property + def icon(self) -> str: + """Return switch icon.""" + return "mdi:wifi" if self.is_on else "mdi:wifi-off" + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the state attributes.""" + return {"ssid": self.router.data[self.key].get("WifiSsid")} diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index c01a5cb4bb8..e1a26405396 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -9,7 +9,7 @@ "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_url": "\u00c9rv\u00e9nytelen URL", - "login_attempts_exceeded": "T\u00fall\u00e9pte a maxim\u00e1lis bejelentkez\u00e9si k\u00eds\u00e9rleteket. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb", + "login_attempts_exceeded": "T\u00fall\u00e9pte a maxim\u00e1lis bejelentkez\u00e9si k\u00eds\u00e9rleteket. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb.", "response_error": "Ismeretlen hiba az eszk\u00f6zr\u0151l", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/huawei_lte/translations/pt.json b/homeassistant/components/huawei_lte/translations/pt.json index be10fe829f7..c8856eb2f79 100644 --- a/homeassistant/components/huawei_lte/translations/pt.json +++ b/homeassistant/components/huawei_lte/translations/pt.json @@ -13,7 +13,7 @@ "data": { "password": "Palavra-passe", "url": "", - "username": "Nome do utilizador" + "username": "Nome de Utilizador" }, "title": "Configurar o Huawei LTE" } diff --git a/homeassistant/components/huawei_lte/utils.py b/homeassistant/components/huawei_lte/utils.py index 37f9f1a5542..bbcf29e552c 100644 --- a/homeassistant/components/huawei_lte/utils.py +++ b/homeassistant/components/huawei_lte/utils.py @@ -14,7 +14,10 @@ def get_device_macs( :param device_info: the device.information structure for the device :param wlan_settings: the wlan.multi_basic_settings structure for the device """ - macs = [device_info.get("MacAddress1"), device_info.get("MacAddress2")] + macs = [ + device_info.get(x) + for x in ("MacAddress1", "MacAddress2", "WifiMacAddrWl0", "WifiMacAddrWl1") + ] try: macs.extend(x.get("WifiMac") for x in wlan_settings["Ssids"]["Ssid"]) except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index e15da5c8489..625a623105f 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -100,7 +100,7 @@ class HueBridge: if self.api_version == 1: if self.api.sensors is not None: self.sensor_manager = SensorManager(self) - self.hass.config_entries.async_setup_platforms( + await self.hass.config_entries.async_forward_entry_setups( self.config_entry, PLATFORMS_v1 ) @@ -108,7 +108,7 @@ class HueBridge: else: await async_setup_devices(self) await async_setup_hue_events(self) - self.hass.config_entries.async_setup_platforms( + await self.hass.config_entries.async_forward_entry_setups( self.config_entry, PLATFORMS_v2 ) diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index 68da5279508..93617d8c0e5 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -39,6 +39,7 @@ "2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "double_buttons_1_3": "\u041f\u044a\u0440\u0432\u0438 \u0438 \u0442\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "double_buttons_2_4": "\u0412\u0442\u043e\u0440\u0438 \u0438 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438" } diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index 09d839cbd5c..9b982d6c84e 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -32,6 +32,15 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primeiro bot\u00e3o", + "button_4": "Quarto bot\u00e3o" + }, + "trigger_type": { + "short_release": "Bot\u00e3o \"{subtype}\" solto ap\u00f3s press\u00e3o curta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index f2b4ef8d4ef..fa810d823ca 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_COORDINATOR: coordinator} # Offload the loading of entities to the platform - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/huisbaasje/translations/pt.json b/homeassistant/components/huisbaasje/translations/pt.json index 3b5850222d9..a2f32087684 100644 --- a/homeassistant/components/huisbaasje/translations/pt.json +++ b/homeassistant/components/huisbaasje/translations/pt.json @@ -1,7 +1,16 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/hu.json b/homeassistant/components/humidifier/translations/hu.json index 6572f0e4955..8c8312dc627 100644 --- a/homeassistant/components/humidifier/translations/hu.json +++ b/homeassistant/components/humidifier/translations/hu.json @@ -14,7 +14,7 @@ }, "trigger_type": { "changed_states": "{entity_name} be- vagy kikapcsolt", - "target_humidity_changed": "{name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott", + "target_humidity_changed": "{entity_name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" } diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 4a22bc4ed81..f7c8cd1eb4b 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_info=device_info, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index 3c090249bc0..1104359111c 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = hub - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 292c426ba19..ec478883e8f 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -269,21 +269,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } ) - async def setup_then_listen() -> None: - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - assert hyperion_client - if hyperion_client.instances is not None: - await async_instances_to_clients_raw(hyperion_client.instances) - hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append( - entry.add_update_listener(_async_entry_updated) - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + assert hyperion_client + if hyperion_client.instances is not None: + await async_instances_to_clients_raw(hyperion_client.instances) + hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append( + entry.add_update_listener(_async_entry_updated) + ) - hass.async_create_task(setup_then_listen()) return True diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index aaffa705279..abb02b1f6e7 100644 --- a/homeassistant/components/hyperion/translations/hu.json +++ b/homeassistant/components/hyperion/translations/hu.json @@ -27,7 +27,7 @@ "title": "Er\u0151s\u00edtse meg a Hyperion Ambilight szolg\u00e1ltat\u00e1s hozz\u00e1ad\u00e1s\u00e1t" }, "create_token": { - "description": "Az al\u00e1bbiakban v\u00e1lassza a **Mehet** lehet\u0151s\u00e9get \u00faj hiteles\u00edt\u00e9si token k\u00e9r\u00e9s\u00e9hez. A k\u00e9relem j\u00f3v\u00e1hagy\u00e1s\u00e1hoz \u00e1tir\u00e1ny\u00edtunk a Hyperion felhaszn\u00e1l\u00f3i fel\u00fcletre. K\u00e9rj\u00fck, ellen\u0151rizze, hogy a megjelen\u00edtett azonos\u00edt\u00f3 \"{auth_id}\"", + "description": "Az al\u00e1bbiakban v\u00e1lassza a **Mehet** lehet\u0151s\u00e9get \u00faj hiteles\u00edt\u00e9si token k\u00e9r\u00e9s\u00e9hez. A k\u00e9relem j\u00f3v\u00e1hagy\u00e1s\u00e1hoz \u00e1tir\u00e1ny\u00edtunk a Hyperion felhaszn\u00e1l\u00f3i fel\u00fcletre. K\u00e9rem, ellen\u0151rizze, hogy a megjelen\u00edtett azonos\u00edt\u00f3 \"{auth_id}\"", "title": "\u00daj hiteles\u00edt\u00e9si token automatikus l\u00e9trehoz\u00e1sa" }, "create_token_external": { diff --git a/homeassistant/components/hyperion/translations/pt.json b/homeassistant/components/hyperion/translations/pt.json index ac9710c6b9b..0a402a87037 100644 --- a/homeassistant/components/hyperion/translations/pt.json +++ b/homeassistant/components/hyperion/translations/pt.json @@ -10,6 +10,11 @@ "invalid_access_token": "Token de acesso inv\u00e1lido" }, "step": { + "auth": { + "data": { + "create_token": "Criar novo token automaticamente" + } + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index 254bf6f685f..374ba29dccf 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ialarm/translations/bg.json b/homeassistant/components/ialarm/translations/bg.json index 4983c9a14b2..09f0ff26e5d 100644 --- a/homeassistant/components/ialarm/translations/bg.json +++ b/homeassistant/components/ialarm/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/ialarm/translations/pt.json b/homeassistant/components/ialarm/translations/pt.json new file mode 100644 index 00000000000..0c5c7760566 --- /dev/null +++ b/homeassistant/components/ialarm/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 02e27d8e82f..844313a4aed 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -117,22 +117,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: elif isinstance(dev, AqualinkToggle): switches += [dev] - forward_setup = hass.config_entries.async_forward_entry_setup + platforms = [] if binary_sensors: _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors), binary_sensors) - hass.async_create_task(forward_setup(entry, Platform.BINARY_SENSOR)) + platforms.append(Platform.BINARY_SENSOR) if climates: _LOGGER.debug("Got %s climates: %s", len(climates), climates) - hass.async_create_task(forward_setup(entry, Platform.CLIMATE)) + platforms.append(Platform.CLIMATE) if lights: _LOGGER.debug("Got %s lights: %s", len(lights), lights) - hass.async_create_task(forward_setup(entry, Platform.LIGHT)) + platforms.append(Platform.LIGHT) if sensors: _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors) - hass.async_create_task(forward_setup(entry, Platform.SENSOR)) + platforms.append(Platform.SENSOR) if switches: _LOGGER.debug("Got %s switches: %s", len(switches), switches) - hass.async_create_task(forward_setup(entry, Platform.SWITCH)) + platforms.append(Platform.SWITCH) + + await hass.config_entries.async_forward_entry_setups(entry, platforms) async def _async_systems_update(now): """Refresh internal state for all systems.""" diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json index 4ee5ba39b8b..7bbac17747a 100644 --- a/homeassistant/components/iaqualink/translations/hu.json +++ b/homeassistant/components/iaqualink/translations/hu.json @@ -13,7 +13,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg iAqualink-fi\u00f3kj\u00e1nak felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", + "description": "K\u00e9rem, adja meg iAqualink-fi\u00f3kj\u00e1nak felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", "title": "Csatlakoz\u00e1s az iAqualinkhez" } } diff --git a/homeassistant/components/iaqualink/translations/ja.json b/homeassistant/components/iaqualink/translations/ja.json index 20c3551ff80..968f4023522 100644 --- a/homeassistant/components/iaqualink/translations/ja.json +++ b/homeassistant/components/iaqualink/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/iaqualink/translations/pt.json b/homeassistant/components/iaqualink/translations/pt.json index 3b466866334..f73c9be5561 100644 --- a/homeassistant/components/iaqualink/translations/pt.json +++ b/homeassistant/components/iaqualink/translations/pt.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Palavra-passe", - "username": "Nome de utilizador / Endere\u00e7o de e-mail" + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 8175cf43f27..63802804f4d 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,4 +1,8 @@ """The iCloud component.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -82,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=username) - icloud_dir = Store(hass, STORAGE_VERSION, STORAGE_KEY) + icloud_dir = Store[Any](hass, STORAGE_VERSION, STORAGE_KEY) account = IcloudAccount( hass, @@ -98,12 +102,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.unique_id] = account - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def play_sound(service: ServiceCall) -> None: """Play sound on the device.""" account = service.data[ATTR_ACCOUNT] - device_name = service.data.get(ATTR_DEVICE_NAME) + device_name: str = service.data[ATTR_DEVICE_NAME] device_name = slugify(device_name.replace(" ", "", 99)) for device in _get_account(account).get_devices_with_name(device_name): @@ -112,7 +116,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def display_message(service: ServiceCall) -> None: """Display a message on the device.""" account = service.data[ATTR_ACCOUNT] - device_name = service.data.get(ATTR_DEVICE_NAME) + device_name: str = service.data[ATTR_DEVICE_NAME] device_name = slugify(device_name.replace(" ", "", 99)) message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) sound = service.data.get(ATTR_LOST_DEVICE_SOUND, False) @@ -123,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def lost_device(service: ServiceCall) -> None: """Make the device in lost state.""" account = service.data[ATTR_ACCOUNT] - device_name = service.data.get(ATTR_DEVICE_NAME) + device_name: str = service.data[ATTR_DEVICE_NAME] device_name = slugify(device_name.replace(" ", "", 99)) number = service.data.get(ATTR_LOST_DEVICE_NUMBER) message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) @@ -139,11 +143,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: _get_account(account).keep_alive() - def _get_account(account_identifier: str) -> any: + def _get_account(account_identifier: str) -> IcloudAccount: if account_identifier is None: return None - icloud_account = hass.data[DOMAIN].get(account_identifier) + icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) if icloud_account is None: for account in hass.data[DOMAIN].values(): if account.username == account_identifier: diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 95b90791165..4dc3c07aba7 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -91,21 +91,19 @@ class IcloudAccount: self._username = username self._password = password self._with_family = with_family - self._fetch_interval = max_interval + self._fetch_interval: float = max_interval self._max_interval = max_interval self._gps_accuracy_threshold = gps_accuracy_threshold self._icloud_dir = icloud_dir self.api: PyiCloudService | None = None - self._owner_fullname = None - self._family_members_fullname = {} - self._devices = {} + self._owner_fullname: str | None = None + self._family_members_fullname: dict[str, str] = {} + self._devices: dict[str, IcloudDevice] = {} self._retried_fetch = False self._config_entry = config_entry - self.listeners = [] - def setup(self) -> None: """Set up an iCloud account.""" try: @@ -271,6 +269,8 @@ class IcloudAccount: distances = [] for zone_state in zones: + if zone_state is None: + continue zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE] zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE] zone_distance = distance( @@ -279,7 +279,8 @@ class IcloudAccount: zone_state_lat, zone_state_long, ) - distances.append(round(zone_distance / 1000, 1)) + if zone_distance is not None: + distances.append(round(zone_distance / 1000, 1)) # Max interval if no zone if not distances: @@ -288,7 +289,7 @@ class IcloudAccount: # Calculate out how long it would take for the device to drive # to the nearest zone at 120 km/h: - interval = round(mindistance / 2, 0) + interval = round(mindistance / 2) # Never poll more than once per minute interval = max(interval, 1) @@ -324,7 +325,7 @@ class IcloudAccount: self.api.authenticate() self.update_devices() - def get_devices_with_name(self, name: str) -> [any]: + def get_devices_with_name(self, name: str) -> list[Any]: """Get devices by name.""" result = [] name_slug = slugify(name.replace(" ", "", 99)) @@ -341,7 +342,7 @@ class IcloudAccount: return self._username @property - def owner_fullname(self) -> str: + def owner_fullname(self) -> str | None: """Return the account owner fullname.""" return self._owner_fullname @@ -351,7 +352,7 @@ class IcloudAccount: return self._family_members_fullname @property - def fetch_interval(self) -> int: + def fetch_interval(self) -> float: """Return the account fetch interval.""" return self._fetch_interval @@ -386,14 +387,7 @@ class IcloudDevice: self._device_class = self._status[DEVICE_CLASS] self._device_model = self._status[DEVICE_DISPLAY_NAME] - if self._status[DEVICE_PERSON_ID]: - owner_fullname = account.family_members_fullname[ - self._status[DEVICE_PERSON_ID] - ] - else: - owner_fullname = account.owner_fullname - - self._battery_level = None + self._battery_level: int | None = None self._battery_status = None self._location = None @@ -402,8 +396,13 @@ class IcloudDevice: ATTR_ACCOUNT_FETCH_INTERVAL: self._account.fetch_interval, ATTR_DEVICE_NAME: self._device_model, ATTR_DEVICE_STATUS: None, - ATTR_OWNER_NAME: owner_fullname, } + if self._status[DEVICE_PERSON_ID]: + self._attrs[ATTR_OWNER_NAME] = account.family_members_fullname[ + self._status[DEVICE_PERSON_ID] + ] + elif account.owner_fullname is not None: + self._attrs[ATTR_OWNER_NAME] = account.owner_fullname def update(self, status) -> None: """Update the iCloud device.""" @@ -487,17 +486,17 @@ class IcloudDevice: return self._device_model @property - def battery_level(self) -> int: + def battery_level(self) -> int | None: """Return the Apple device battery level.""" return self._battery_level @property - def battery_status(self) -> str: + def battery_status(self) -> str | None: """Return the Apple device battery status.""" return self._battery_status @property - def location(self) -> dict[str, Any]: + def location(self) -> dict[str, Any] | None: """Return the Apple device location.""" return self._location diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index c9d251b06c7..9c2004f0edb 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -36,7 +36,7 @@ async def async_setup_entry( ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] - tracked = set() + tracked = set[str]() @callback def update_account(): @@ -51,7 +51,7 @@ async def async_setup_entry( @callback -def add_entities(account, async_add_entities, tracked): +def add_entities(account: IcloudAccount, async_add_entities, tracked): """Add new tracker entities from the account.""" new_tracked = [] @@ -101,7 +101,7 @@ class IcloudTrackerEntity(TrackerEntity): return self._device.location[DEVICE_LOCATION_LONGITUDE] @property - def battery_level(self) -> int: + def battery_level(self) -> int | None: """Return the battery level of the device.""" return self._device.battery_level @@ -148,7 +148,7 @@ def icon_for_icloud_device(icloud_device: IcloudDevice) -> str: "iPad": "mdi:tablet", "iPhone": "mdi:cellphone", "iPod": "mdi:ipod", - "iMac": "mdi:desktop-mac", + "iMac": "mdi:monitor", "MacBookPro": "mdi:laptop", } diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 38ea3af62b6..6e415aa3350 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -21,7 +21,7 @@ async def async_setup_entry( ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] - tracked = set() + tracked = set[str]() @callback def update_account(): @@ -74,7 +74,7 @@ class IcloudDeviceBatterySensor(SensorEntity): return f"{self._device.name} battery state" @property - def native_value(self) -> int: + def native_value(self) -> int | None: """Battery state percentage.""" return self._device.battery_level diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index cff637ae03f..1cbbbfb6974 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -38,7 +38,7 @@ "data": { "verification_code": "Ellen\u0151rz\u0151 k\u00f3d" }, - "description": "K\u00e9rj\u00fck, \u00edrja be az iCloud-t\u00f3l \u00e9ppen kapott ellen\u0151rz\u0151 k\u00f3dot", + "description": "K\u00e9rem, \u00edrja be az iCloud-t\u00f3l \u00e9ppen kapott ellen\u0151rz\u0151 k\u00f3dot", "title": "iCloud ellen\u0151rz\u0151 k\u00f3d" } } diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index 4b7947b8dfa..2295e72f0b2 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -15,8 +15,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u7d71\u5408\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "trusted_device": { "data": { diff --git a/homeassistant/components/ifttt/translations/ja.json b/homeassistant/components/ifttt/translations/ja.json index 87844be51d4..05353ba44a1 100644 --- a/homeassistant/components/ifttt/translations/ja.json +++ b/homeassistant/components/ifttt/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index cca3e61a5a3..1194c58d2ca 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -1,15 +1,19 @@ """Support for IGN Sismologia (Earthquakes) Feeds.""" from __future__ import annotations +from collections.abc import Callable from datetime import timedelta import logging +from typing import Any -from georss_ign_sismologia_client import IgnSismologiaFeedManager +from georss_ign_sismologia_client import ( + IgnSismologiaFeedEntry, + IgnSismologiaFeedManager, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, @@ -17,7 +21,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -61,19 +65,19 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the IGN Sismologia Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE] + radius_in_km: float = config[CONF_RADIUS] + minimum_magnitude: float = config[CONF_MINIMUM_MAGNITUDE] # Initialize the entity manager. feed = IgnSismologiaFeedEntityManager( hass, add_entities, scan_interval, coordinates, radius_in_km, minimum_magnitude ) - def start_feed_manager(event): + def start_feed_manager(event: Event) -> None: """Start feed manager.""" feed.startup() @@ -85,13 +89,13 @@ class IgnSismologiaFeedEntityManager: def __init__( self, - hass, - add_entities, - scan_interval, - coordinates, - radius_in_km, - minimum_magnitude, - ): + hass: HomeAssistant, + add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + radius_in_km: float, + minimum_magnitude: float, + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass @@ -106,32 +110,32 @@ class IgnSismologiaFeedEntityManager: self._add_entities = add_entities self._scan_interval = scan_interval - def startup(self): + def startup(self) -> None: """Start up this manager.""" self._feed_manager.update() self._init_regular_updates() - def _init_regular_updates(self): + def _init_regular_updates(self) -> None: """Schedule regular updates at the specified interval.""" track_time_interval( self._hass, lambda now: self._feed_manager.update(), self._scan_interval ) - def get_entry(self, external_id): + def get_entry(self, external_id: str) -> IgnSismologiaFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = IgnSismologiaLocationEvent(self, external_id) # Add new entities to HA. self._add_entities([new_entity], True) - def _update_entity(self, external_id): + def _update_entity(self, external_id: str) -> None: """Update entity.""" dispatcher_send(self._hass, f"ign_sismologia_update_{external_id}") - def _remove_entity(self, external_id): + def _remove_entity(self, external_id: str) -> None: """Remove entity.""" dispatcher_send(self._hass, f"ign_sismologia_delete_{external_id}") @@ -139,25 +143,26 @@ class IgnSismologiaFeedEntityManager: class IgnSismologiaLocationEvent(GeolocationEvent): """This represents an external event with IGN Sismologia feed data.""" + _attr_icon = "mdi:pulse" + _attr_should_poll = False + _attr_source = SOURCE _attr_unit_of_measurement = LENGTH_KILOMETERS - def __init__(self, feed_manager, external_id): + def __init__( + self, feed_manager: IgnSismologiaFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._region = None self._magnitude = None self._publication_date = None self._image_url = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -171,51 +176,36 @@ class IgnSismologiaLocationEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for IGN Sismologia feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: IgnSismologiaFeedEntry) -> None: """Update the internal state from the provided feed entry.""" self._title = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._region = feed_entry.region self._magnitude = feed_entry.magnitude self._publication_date = feed_entry.published self._image_url = feed_entry.image_url - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - @property def name(self) -> str | None: """Return the name of the entity.""" @@ -228,22 +218,7 @@ class IgnSismologiaLocationEvent(GeolocationEvent): return self._title @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( @@ -251,7 +226,6 @@ class IgnSismologiaLocationEvent(GeolocationEvent): (ATTR_TITLE, self._title), (ATTR_REGION, self._region), (ATTR_MAGNITUDE, self._magnitude), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_PUBLICATION_DATE, self._publication_date), (ATTR_IMAGE_URL, self._image_url), ): diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index d2e08b77b23..4f967dbcc89 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==9.1.1"], + "requirements": ["pillow==9.2.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index ecc76481e49..72871c75fc4 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -30,7 +30,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import event as event_helper, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues @@ -198,13 +198,13 @@ CONFIG_SCHEMA = vol.Schema( ) -def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: +def _generate_event_to_json(conf: dict) -> Callable[[Event], dict[str, Any] | None]: """Build event to json converter and add to config.""" entity_filter = convert_include_exclude_filter(conf) tags = conf.get(CONF_TAGS) - tags_attributes = conf.get(CONF_TAGS_ATTRIBUTES) + tags_attributes: list[str] = conf[CONF_TAGS_ATTRIBUTES] default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT) - measurement_attr = conf.get(CONF_MEASUREMENT_ATTR) + measurement_attr: str = conf[CONF_MEASUREMENT_ATTR] override_measurement = conf.get(CONF_OVERRIDE_MEASUREMENT) global_ignore_attributes = set(conf[CONF_IGNORE_ATTRIBUTES]) component_config = EntityValues( @@ -213,15 +213,15 @@ def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: conf[CONF_COMPONENT_CONFIG_GLOB], ) - def event_to_json(event: dict) -> str: + def event_to_json(event: Event) -> dict[str, Any] | None: """Convert event into json in format Influx expects.""" - state = event.data.get(EVENT_NEW_STATE) + state: State | None = event.data.get(EVENT_NEW_STATE) if ( state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) or not entity_filter(state.entity_id) ): - return + return None try: _include_state = _include_value = False @@ -263,7 +263,7 @@ def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: else: include_uom = measurement_attr != "unit_of_measurement" - json = { + json: dict[str, Any] = { INFLUX_CONF_MEASUREMENT: measurement, INFLUX_CONF_TAGS: { CONF_DOMAIN: state.domain, @@ -328,7 +328,9 @@ class InfluxClient: close: Callable[[], None] -def get_influx_connection(conf, test_write=False, test_read=False): # noqa: C901 +def get_influx_connection( # noqa: C901 + conf, test_write=False, test_read=False +) -> InfluxClient: """Create the correct influx connection for the API version.""" kwargs = { CONF_TIMEOUT: TIMEOUT, @@ -470,6 +472,10 @@ def get_influx_connection(conf, test_write=False, test_read=False): # noqa: C90 return InfluxClient(databases, write_v1, query_v1, close_v1) +def _retry_setup(hass: HomeAssistant, config: ConfigType) -> None: + setup(hass, config) + + def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the InfluxDB component.""" conf = config[DOMAIN] @@ -477,7 +483,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: influx = get_influx_connection(conf, test_write=True) except ConnectionError as exc: _LOGGER.error(RETRY_MESSAGE, exc) - event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) + event_helper.call_later( + hass, RETRY_INTERVAL, lambda _: _retry_setup(hass, config) + ) return True event_to_json = _generate_event_to_json(conf) diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py new file mode 100644 index 00000000000..0272114b83c --- /dev/null +++ b/homeassistant/components/inkbird/__init__.py @@ -0,0 +1,42 @@ +"""The INKBIRD Bluetooth integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up INKBIRD BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py new file mode 100644 index 00000000000..21ed85e117e --- /dev/null +++ b/homeassistant/components/inkbird/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for inkbird ble integration.""" +from __future__ import annotations + +from typing import Any + +from inkbird_ble import INKBIRDBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for inkbird.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/inkbird/const.py b/homeassistant/components/inkbird/const.py new file mode 100644 index 00000000000..9d0e1638958 --- /dev/null +++ b/homeassistant/components/inkbird/const.py @@ -0,0 +1,3 @@ +"""Constants for the INKBIRD Bluetooth integration.""" + +DOMAIN = "inkbird" diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json new file mode 100644 index 00000000000..686c9bada2d --- /dev/null +++ b/homeassistant/components/inkbird/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "inkbird", + "name": "INKBIRD", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/inkbird", + "bluetooth": [ + { "local_name": "sps" }, + { "local_name": "Inkbird*" }, + { "local_name": "iBBQ*" }, + { "local_name": "tps" } + ], + "requirements": ["inkbird-ble==0.5.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py new file mode 100644 index 00000000000..0648ca80383 --- /dev/null +++ b/homeassistant/components/inkbird/sensor.py @@ -0,0 +1,157 @@ +"""Support for inkbird ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from inkbird_ble import ( + DeviceClass, + DeviceKey, + INKBIRDBluetoothDeviceData, + SensorDeviceInfo, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the INKBIRD BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + data = INKBIRDBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload( + processor.async_add_entities_listener( + INKBIRDBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class INKBIRDBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a inkbird ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/inkbird/strings.json b/homeassistant/components/inkbird/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/inkbird/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "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%]" + } + } +} diff --git a/homeassistant/components/inkbird/translations/ca.json b/homeassistant/components/inkbird/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/inkbird/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/de.json b/homeassistant/components/inkbird/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/inkbird/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/el.json b/homeassistant/components/inkbird/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/inkbird/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/en.json b/homeassistant/components/inkbird/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/inkbird/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/et.json b/homeassistant/components/inkbird/translations/et.json new file mode 100644 index 00000000000..749f7e45de5 --- /dev/null +++ b/homeassistant/components/inkbird/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/fr.json b/homeassistant/components/inkbird/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/inkbird/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/hu.json b/homeassistant/components/inkbird/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/inkbird/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/id.json b/homeassistant/components/inkbird/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/inkbird/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/it.json b/homeassistant/components/inkbird/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/inkbird/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/ja.json b/homeassistant/components/inkbird/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/inkbird/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/pl.json b/homeassistant/components/inkbird/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/inkbird/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/pt-BR.json b/homeassistant/components/inkbird/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/inkbird/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/ru.json b/homeassistant/components/inkbird/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/inkbird/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/zh-Hant.json b/homeassistant/components/inkbird/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/inkbird/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index fe034a8edb5..2a64c8d3b89 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import datetime as py_datetime import logging +from typing import Any import voluptuous as vol @@ -84,23 +85,32 @@ def has_date_or_time(conf): raise vol.Invalid("Entity needs at least a date or a time") -def valid_initial(conf): +def valid_initial(conf: dict[str, Any]) -> dict[str, Any]: """Check the initial value is valid.""" - if not (initial := conf.get(CONF_INITIAL)): + if not (conf.get(CONF_INITIAL)): return conf + # Ensure we can parse the initial value, raise vol.Invalid on failure + parse_initial_datetime(conf) + return conf + + +def parse_initial_datetime(conf: dict[str, Any]) -> py_datetime.datetime: + """Check the initial value is valid.""" + initial: str = conf[CONF_INITIAL] + if conf[CONF_HAS_DATE] and conf[CONF_HAS_TIME]: - if dt_util.parse_datetime(initial) is not None: - return conf + if (datetime := dt_util.parse_datetime(initial)) is not None: + return datetime raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a datetime") if conf[CONF_HAS_DATE]: - if dt_util.parse_date(initial) is not None: - return conf + if (date := dt_util.parse_date(initial)) is not None: + return py_datetime.datetime.combine(date, DEFAULT_TIME) raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a date") - if dt_util.parse_time(initial) is not None: - return conf + if (time := dt_util.parse_time(initial)) is not None: + return py_datetime.datetime.combine(py_datetime.date.today(), time) raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a time") @@ -230,21 +240,10 @@ class InputDatetime(RestoreEntity): self.editable = True self._current_datetime = None - if not (initial := config.get(CONF_INITIAL)): + if not config.get(CONF_INITIAL): return - if self.has_date and self.has_time: - current_datetime = dt_util.parse_datetime(initial) - - elif self.has_date: - date = dt_util.parse_date(initial) - current_datetime = py_datetime.datetime.combine(date, DEFAULT_TIME) - - else: - time = dt_util.parse_time(initial) - current_datetime = py_datetime.datetime.combine( - py_datetime.date.today(), time - ) + current_datetime = parse_initial_datetime(config) # If the user passed in an initial value with a timezone, convert it to right tz if current_datetime.tzinfo is not None: diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index e2bebf0bb7f..82b910215c4 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -154,10 +154,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) device = devices.add_x10_device(housecode, unitcode, x10_type, steps) - for platform in INSTEON_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + await hass.config_entries.async_forward_entry_setups(entry, INSTEON_PLATFORMS) for address in devices: device = devices[address] diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index fb7b2387d73..5337ccd36c3 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -44,6 +44,7 @@ INSTEON_PLATFORMS = [ Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SWITCH, ] diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 6866e052368..7f4ff92380f 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -1,5 +1,6 @@ """Utility methods for the Insteon platform.""" from pyinsteon.device_types import ( + AccessControl_Morningstar, ClimateControl_Thermostat, ClimateControl_WirelessThermostat, DimmableLightingControl, @@ -12,6 +13,7 @@ from pyinsteon.device_types import ( DimmableLightingControl_OutletLinc, DimmableLightingControl_SwitchLinc, DimmableLightingControl_ToggleLinc, + EnergyManagement_LoadController, GeneralController_ControlLinc, GeneralController_MiniRemote_4, GeneralController_MiniRemote_8, @@ -44,11 +46,13 @@ from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.switch import DOMAIN as SWITCH from .const import ON_OFF_EVENTS DEVICE_PLATFORM = { + AccessControl_Morningstar: {LOCK: [1]}, DimmableLightingControl: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_DinRail: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_FanLinc: {LIGHT: [1], FAN: [2], ON_OFF_EVENTS: [1, 2]}, @@ -67,6 +71,7 @@ DEVICE_PLATFORM = { DimmableLightingControl_OutletLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_SwitchLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_ToggleLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + EnergyManagement_LoadController: {SWITCH: [1], BINARY_SENSOR: [2]}, GeneralController_ControlLinc: {ON_OFF_EVENTS: [1]}, GeneralController_MiniRemote_4: {ON_OFF_EVENTS: range(1, 5)}, GeneralController_MiniRemote_8: {ON_OFF_EVENTS: range(1, 9)}, diff --git a/homeassistant/components/insteon/lock.py b/homeassistant/components/insteon/lock.py new file mode 100644 index 00000000000..17a7cf20111 --- /dev/null +++ b/homeassistant/components/insteon/lock.py @@ -0,0 +1,49 @@ +"""Support for INSTEON locks.""" + +from typing import Any + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import SIGNAL_ADD_ENTITIES +from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Insteon locks from a config entry.""" + + @callback + def async_add_insteon_lock_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, LOCK_DOMAIN, InsteonLockEntity, async_add_entities, discovery_info + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{LOCK_DOMAIN}" + async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities) + async_add_insteon_lock_entities() + + +class InsteonLockEntity(InsteonEntity, LockEntity): + """A Class for an Insteon lock entity.""" + + @property + def is_locked(self) -> bool: + """Return the boolean response if the node is on.""" + return bool(self._insteon_device_group.value) + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the device.""" + await self._insteon_device.async_lock() + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the device.""" + await self._insteon_device.async_unlock() diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c48d502c16e..577383e8976 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.3", + "pyinsteon==1.2.0", "insteon-frontend-home-assistant==0.2.0" ], "codeowners": ["@teharris1"], diff --git a/homeassistant/components/insteon/translations/bg.json b/homeassistant/components/insteon/translations/bg.json index e1adb8657da..f3fb9df607c 100644 --- a/homeassistant/components/insteon/translations/bg.json +++ b/homeassistant/components/insteon/translations/bg.json @@ -34,6 +34,11 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "add_x10": { + "data": { + "platform": "\u041f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430" + } + }, "change_hub_config": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 812f9c0bfd6..f5b41709d71 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_insteon_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Insteon\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json index 1a281774ffe..4654d2c4de1 100644 --- a/homeassistant/components/insteon/translations/pt.json +++ b/homeassistant/components/insteon/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" @@ -60,8 +60,14 @@ }, "init": { "data": { + "add_x10": "Adicionar um dispositivo X10.", "remove_x10": "Remova um dispositivo X10." } + }, + "remove_override": { + "data": { + "address": "Seleccione um endere\u00e7o de dispositivo para remover" + } } } } diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index 84bc28e3d2f..f482f4e41e8 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Integration from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/integration/translations/ja.json b/homeassistant/components/integration/translations/ja.json index 4f35ba53a4a..237d7eef353 100644 --- a/homeassistant/components/integration/translations/ja.json +++ b/homeassistant/components/integration/translations/ja.json @@ -16,7 +16,7 @@ "unit_time": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" }, "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u5408\u8a08\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u7a4d\u5206\u6642\u9593\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u30bb\u30f3\u30b5\u30fc" + "title": "\u65b0\u3057\u3044\u7d71\u5408\u30bb\u30f3\u30b5\u30fc" } } }, @@ -32,5 +32,5 @@ } } }, - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 - \u30ea\u30fc\u30de\u30f3\u548c\u7a4d\u5206\u30bb\u30f3\u30b5\u30fc" + "title": "\u7d71\u5408 - \u30ea\u30fc\u30de\u30f3\u548c\u7a4d\u5206\u30bb\u30f3\u30b5\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/pt.json b/homeassistant/components/integration/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/integration/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 034e74c2aa6..f5b6085781b 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -20,7 +20,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import CONF_USER_ID, DOMAIN, LOGGER from .coordinator import IntellifireDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.CLIMATE, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py new file mode 100644 index 00000000000..a00a20f64f1 --- /dev/null +++ b/homeassistant/components/intellifire/climate.py @@ -0,0 +1,114 @@ +"""Intellifire Climate Entities.""" +from __future__ import annotations + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityDescription, + ClimateEntityFeature, +) +from homeassistant.components.climate.const import HVACMode +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import IntellifireDataUpdateCoordinator +from .const import DEFAULT_THERMOSTAT_TEMP, DOMAIN, LOGGER +from .entity import IntellifireEntity + +INTELLIFIRE_CLIMATES: tuple[ClimateEntityDescription, ...] = ( + ClimateEntityDescription(key="climate", name="Thermostat"), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Configure the fan entry..""" + coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if coordinator.data.has_thermostat: + async_add_entities( + IntellifireClimate( + coordinator=coordinator, + description=description, + ) + for description in INTELLIFIRE_CLIMATES + ) + + +class IntellifireClimate(IntellifireEntity, ClimateEntity): + """Intellifire climate entity.""" + + entity_description: ClimateEntityDescription + + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] + _attr_min_temp = 0 + _attr_max_temp = 37 + _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_target_temperature_step = 1.0 + _attr_temperature_unit = TEMP_CELSIUS + last_temp = DEFAULT_THERMOSTAT_TEMP + + def __init__( + self, + coordinator: IntellifireDataUpdateCoordinator, + description: ClimateEntityDescription, + ) -> None: + """Configure climate entry - and override last_temp if the thermostat is currently on.""" + super().__init__(coordinator, description) + + if coordinator.data.thermostat_on: + self.last_temp = coordinator.data.thermostat_setpoint_c + + @property + def hvac_mode(self) -> str: + """Return current hvac mode.""" + if self.coordinator.read_api.data.thermostat_on: + return HVACMode.HEAT + return HVACMode.OFF + + async def async_set_temperature(self, **kwargs) -> None: + """Turn on thermostat by setting a target temperature.""" + raw_target_temp = kwargs[ATTR_TEMPERATURE] + self.last_temp = int(raw_target_temp) + LOGGER.debug( + "Setting target temp to %sc %sf", + int(raw_target_temp), + (raw_target_temp * 9 / 5) + 32, + ) + await self.coordinator.control_api.set_thermostat_c( + temp_c=self.last_temp, + ) + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return float(self.coordinator.read_api.data.temperature_c) + + @property + def target_temperature(self) -> float: + """Return target temperature.""" + return float(self.coordinator.read_api.data.thermostat_setpoint_c) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set HVAC mode to normal or thermostat control.""" + LOGGER.debug( + "Setting mode to [%s] - using last temp: %s", hvac_mode, self.last_temp + ) + + if hvac_mode == HVACMode.OFF: + await self.coordinator.control_api.turn_off_thermostat() + return + + # hvac_mode == HVACMode.HEAT + # 1) Set the desired target temp + await self.coordinator.control_api.set_thermostat_c( + temp_c=self.last_temp, + ) + + # 2) Make sure the fireplace is on! + if not self.coordinator.read_api.data.is_on: + await self.coordinator.control_api.flame_on() diff --git a/homeassistant/components/intellifire/const.py b/homeassistant/components/intellifire/const.py index 2e9a2fabc06..cae25ea11ae 100644 --- a/homeassistant/components/intellifire/const.py +++ b/homeassistant/components/intellifire/const.py @@ -10,3 +10,5 @@ CONF_USER_ID = "user_id" LOGGER = logging.getLogger(__package__) CONF_SERIAL = "serial" + +DEFAULT_THERMOSTAT_TEMP = 21 diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index 39f197285d4..356ddedf16d 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -45,7 +45,7 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData except (ConnectionError, ClientConnectionError) as exception: raise UpdateFailed from exception - LOGGER.info("Failure Count %d", self._api.failed_poll_attempts) + LOGGER.debug("Failure Count %d", self._api.failed_poll_attempts) if self._api.failed_poll_attempts > 10: LOGGER.debug("Too many polling errors - raising exception") raise UpdateFailed diff --git a/homeassistant/components/intellifire/translations/hu.json b/homeassistant/components/intellifire/translations/hu.json index 2f680f2e776..afd5b99e8d7 100644 --- a/homeassistant/components/intellifire/translations/hu.json +++ b/homeassistant/components/intellifire/translations/hu.json @@ -31,7 +31,7 @@ "data": { "host": "C\u00edm" }, - "description": "A k\u00f6vetkez\u0151 IntelliFire eszk\u00f6z\u00f6k \u00e9szlelve. K\u00e9rj\u00fck, v\u00e1lassza ki, melyiket szeretn\u00e9 konfigur\u00e1lni.", + "description": "A k\u00f6vetkez\u0151 IntelliFire eszk\u00f6z\u00f6k \u00e9szlelve. K\u00e9rem, v\u00e1lassza ki, melyiket szeretn\u00e9 konfigur\u00e1lni.", "title": "Eszk\u00f6z v\u00e1laszt\u00e1sa" } } diff --git a/homeassistant/components/intellifire/translations/pt.json b/homeassistant/components/intellifire/translations/pt.json new file mode 100644 index 00000000000..b86378c5e24 --- /dev/null +++ b/homeassistant/components/intellifire/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "flow_title": "{serial} ({host})", + "step": { + "api_config": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index a70a9908fd8..717effa2bb1 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -287,7 +287,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Set up an iOS entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.http.register_view(iOSIdentifyDeviceView(hass.config.path(CONFIGURATION_FILE))) hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_USER][CONF_PUSH])) diff --git a/homeassistant/components/ios/translations/ja.json b/homeassistant/components/ios/translations/ja.json index 60fbfbea06f..d973d38e176 100644 --- a/homeassistant/components/ios/translations/ja.json +++ b/homeassistant/components/ios/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/ios/translations/pt.json b/homeassistant/components/ios/translations/pt.json index 319ba1e3759..096d42a6503 100644 --- a/homeassistant/components/ios/translations/pt.json +++ b/homeassistant/components/ios/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do componente iOS do Home Assistante \u00e9 necess\u00e1ria." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/iotawatt/__init__.py b/homeassistant/components/iotawatt/__init__.py index 55fc701cffd..9f51382f98e 100644 --- a/homeassistant/components/iotawatt/__init__.py +++ b/homeassistant/components/iotawatt/__init__.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = IotawattUpdater(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/iotawatt/translations/hu.json b/homeassistant/components/iotawatt/translations/hu.json index 5f5d30136da..0c048e0102c 100644 --- a/homeassistant/components/iotawatt/translations/hu.json +++ b/homeassistant/components/iotawatt/translations/hu.json @@ -11,7 +11,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Az IoTawatt eszk\u00f6z hiteles\u00edt\u00e9st ig\u00e9nyel. K\u00e9rj\u00fck, adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t, majd folytassa." + "description": "Az IoTawatt eszk\u00f6z hiteles\u00edt\u00e9st ig\u00e9nyel. K\u00e9rem, adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t, majd folytassa." }, "user": { "data": { diff --git a/homeassistant/components/iotawatt/translations/pt.json b/homeassistant/components/iotawatt/translations/pt.json new file mode 100644 index 00000000000..6e210edaef7 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "auth": { + "data": { + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 1bd948ffa5e..4d675e8cc1d 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -13,7 +13,7 @@ PLATFORMS = [Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPMA station as config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 23ee5adc0e4..42dc2b8d93b 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index 18381fde2cf..471c73c8abc 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -11,7 +11,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra az SSL/TLS opci\u00f3 bejel\u00f6l\u00e9s\u00e9vel." + "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz. K\u00e9rem, esetleg pr\u00f3b\u00e1lja meg \u00fajra az SSL/TLS opci\u00f3 bejel\u00f6l\u00e9s\u00e9vel." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 42da8aadb6d..aad505e23c4 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -118,6 +118,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class IQVIAEntity(CoordinatorEntity): """Define a base IQVIA entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 7485ff9d608..561ebdb6e89 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.23.0", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.23.1", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index d8c7ea317c8..033ed4e3031 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -79,17 +79,17 @@ TREND_SUBSIDING = "Subsiding" FORECAST_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_ALLERGY_FORECAST, - name="Allergy Index: Forecasted Average", + name="Allergy index: forecasted average", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_ASTHMA_FORECAST, - name="Asthma Index: Forecasted Average", + name="Asthma index: forecasted average", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_DISEASE_FORECAST, - name="Cold & Flu: Forecasted Average", + name="Cold & flu: forecasted average", icon="mdi:snowflake", ), ) @@ -97,29 +97,29 @@ FORECAST_SENSOR_DESCRIPTIONS = ( INDEX_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_ALLERGY_TODAY, - name="Allergy Index: Today", + name="Allergy index: today", icon="mdi:flower", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_ALLERGY_TOMORROW, - name="Allergy Index: Tomorrow", + name="Allergy index: tomorrow", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_ASTHMA_TODAY, - name="Asthma Index: Today", + name="Asthma index: today", icon="mdi:flower", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_ASTHMA_TOMORROW, - name="Asthma Index: Tomorrow", + name="Asthma index: tomorrow", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_DISEASE_TODAY, - name="Cold & Flu Index: Today", + name="Cold & flu index: today", icon="mdi:pill", state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/islamic_prayer_times/translations/ja.json b/homeassistant/components/islamic_prayer_times/translations/ja.json index ea6ad0c6522..9c38d5a4aab 100644 --- a/homeassistant/components/islamic_prayer_times/translations/ja.json +++ b/homeassistant/components/islamic_prayer_times/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py index 997c3fff2a3..476a944be81 100644 --- a/homeassistant/components/iss/__init__.py +++ b/homeassistant/components/iss/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/iss/translations/ja.json b/homeassistant/components/iss/translations/ja.json index bf5deeaa716..d53b9f8fecb 100644 --- a/homeassistant/components/iss/translations/ja.json +++ b/homeassistant/components/iss/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "latitude_longitude_not_defined": "Home Assistant\u3067\u7def\u5ea6\u3068\u7d4c\u5ea6\u304c\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 3cee445b587..301c86827e9 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -204,7 +204,7 @@ async def async_setup_entry( _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def _async_stop_auto_update(event: Event) -> None: diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json index e2d3dc4c883..ac6773f09e1 100644 --- a/homeassistant/components/isy994/translations/cs.json +++ b/homeassistant/components/isy994/translations/cs.json @@ -7,6 +7,7 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_host": "Z\u00e1znam hostitele nebyl v \u00fapln\u00e9m form\u00e1tu URL, nap\u0159. http://192.168.10.100:80", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index e5a5c4d6f98..4ea107b469f 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -27,7 +27,7 @@ "tls": "Az ISY vez\u00e9rl\u0151 TLS verzi\u00f3ja.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "A c\u00edm bejegyz\u00e9s\u00e9nek teljes URL form\u00e1tumban kell lennie, pl. Http://192.168.10.100:80", + "description": "A c\u00edm bejegyz\u00e9s\u00e9nek teljes URL form\u00e1tumban kell lennie, pl. http://192.168.10.100:80", "title": "Csatlakozzon az ISY-hez" } } diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index f1a447901b9..60b69d07363 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -41,7 +41,7 @@ "sensor_string": "\u30ce\u30fc\u30c9 \u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217", "variable_sensor_string": "\u53ef\u5909\u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217(Variable Sensor String)" }, - "description": "ISY\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a:\n \u2022 Node Sensor String: \u540d\u524d\u306b\u3001'Node Sensor String' \u3092\u542b\u3080\u4efb\u610f\u306e\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u30d5\u30a9\u30eb\u30c0\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u307e\u305f\u306f\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u6271\u308f\u308c\u307e\u3059\u3002\n \u2022 Ignore String: \u540d\u524d\u306b\u3001'Ignore String'\u3092\u6301\u3064\u30c7\u30d0\u30a4\u30b9\u306f\u7121\u8996\u3055\u308c\u307e\u3059\u3002\n \u2022 Variable Sensor String: 'Variable Sensor String' \u3092\u542b\u3080\u5909\u6570\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u8ffd\u52a0\u3055\u308c\u307e\u3059\u3002\n \u2022 Restore Light Brightness: \u6709\u52b9\u306b\u3059\u308b\u3068\u3001\u30c7\u30d0\u30a4\u30b9\u7d44\u307f\u8fbc\u307f\u306e\u30aa\u30f3\u30ec\u30d9\u30eb\u3067\u306f\u306a\u304f\u3001\u30e9\u30a4\u30c8\u3092\u30aa\u30f3\u306b\u3057\u305f\u3068\u304d\u306b\u3001\u4ee5\u524d\u306e\u660e\u308b\u3055\u306b\u5fa9\u5143\u3055\u308c\u307e\u3059\u3002", + "description": "ISY\u7d71\u5408\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a:\n \u2022 Node Sensor String: \u540d\u524d\u306b\u3001'Node Sensor String' \u3092\u542b\u3080\u4efb\u610f\u306e\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u30d5\u30a9\u30eb\u30c0\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u307e\u305f\u306f\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u6271\u308f\u308c\u307e\u3059\u3002\n \u2022 Ignore String: \u540d\u524d\u306b\u3001'Ignore String'\u3092\u6301\u3064\u30c7\u30d0\u30a4\u30b9\u306f\u7121\u8996\u3055\u308c\u307e\u3059\u3002\n \u2022 Variable Sensor String: 'Variable Sensor String' \u3092\u542b\u3080\u5909\u6570\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u8ffd\u52a0\u3055\u308c\u307e\u3059\u3002\n \u2022 Restore Light Brightness: \u6709\u52b9\u306b\u3059\u308b\u3068\u3001\u30c7\u30d0\u30a4\u30b9\u7d44\u307f\u8fbc\u307f\u306e\u30aa\u30f3\u30ec\u30d9\u30eb\u3067\u306f\u306a\u304f\u3001\u30e9\u30a4\u30c8\u3092\u30aa\u30f3\u306b\u3057\u305f\u3068\u304d\u306b\u3001\u4ee5\u524d\u306e\u660e\u308b\u3055\u306b\u5fa9\u5143\u3055\u308c\u307e\u3059\u3002", "title": "ISY994\u30aa\u30d7\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json index 36962100519..aa4c5f614e6 100644 --- a/homeassistant/components/isy994/translations/pt.json +++ b/homeassistant/components/isy994/translations/pt.json @@ -8,7 +8,13 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, + "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 0aef8360bdd..3f2565bd8f4 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -47,7 +47,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" await async_start_discovery_service(hass) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 419e1709b45..a26490e78c8 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -76,7 +76,7 @@ async def async_setup_entry( @callback def init_controller(ctrl: Controller): """Register the controller device and the containing zones.""" - conf: ConfigType = hass.data.get(DATA_CONFIG) + conf: ConfigType | None = hass.data.get(DATA_CONFIG) # Filter out any entities excluded in the config file if conf and ctrl.device_uid in conf[CONF_EXCLUDE]: @@ -107,8 +107,6 @@ async def async_setup_entry( "async_set_airflow_max", ) - return True - def _return_on_connection_error(ret=None): def wrap(func): @@ -310,7 +308,7 @@ class ControllerDevice(ClimateEntity): return key assert False, "Should be unreachable" - @property + @property # type: ignore[misc] @_return_on_connection_error([]) def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" @@ -318,13 +316,13 @@ class ControllerDevice(ClimateEntity): return [HVACMode.OFF, HVACMode.FAN_ONLY] return [HVACMode.OFF, *self._state_to_pizone] - @property + @property # type: ignore[misc] @_return_on_connection_error(PRESET_NONE) def preset_mode(self): """Eco mode is external air.""" return PRESET_ECO if self._controller.free_air else PRESET_NONE - @property + @property # type: ignore[misc] @_return_on_connection_error([PRESET_NONE]) def preset_modes(self): """Available preset modes, normal or eco.""" @@ -332,7 +330,7 @@ class ControllerDevice(ClimateEntity): return [PRESET_NONE, PRESET_ECO] return [PRESET_NONE] - @property + @property # type: ignore[misc] @_return_on_connection_error() def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -362,7 +360,7 @@ class ControllerDevice(ClimateEntity): return None return zone.target_temperature - @property + @property # type: ignore[misc] @_return_on_connection_error() def target_temperature(self) -> float | None: """Return the temperature we try to reach (either from control zone or master unit).""" @@ -390,13 +388,13 @@ class ControllerDevice(ClimateEntity): """Return the list of available fan modes.""" return list(self._fan_to_pizone) - @property + @property # type: ignore[misc] @_return_on_connection_error(0.0) def min_temp(self) -> float: """Return the minimum temperature.""" return self._controller.temp_min - @property + @property # type: ignore[misc] @_return_on_connection_error(50.0) def max_temp(self) -> float: """Return the maximum temperature.""" @@ -471,7 +469,9 @@ class ZoneDevice(ClimateEntity): self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE self._attr_device_info = DeviceInfo( - identifiers={(IZONE, controller.unique_id, zone.index)}, + identifiers={ + (IZONE, controller.unique_id, zone.index) # type:ignore[arg-type] + }, manufacturer="IZone", model=zone.type.name.title(), name=self.name, @@ -533,7 +533,7 @@ class ZoneDevice(ClimateEntity): """ return False - @property + @property # type: ignore[misc] @_return_on_connection_error(0) def supported_features(self): """Return the list of supported features.""" @@ -552,7 +552,7 @@ class ZoneDevice(ClimateEntity): return PRECISION_TENTHS @property - def hvac_mode(self) -> HVACMode: + def hvac_mode(self) -> HVACMode | None: """Return current operation ie. heat, cool, idle.""" mode = self._zone.mode for (key, value) in self._state_to_pizone.items(): diff --git a/homeassistant/components/izone/translations/ja.json b/homeassistant/components/izone/translations/ja.json index bd5ae39dec4..1699170cbd9 100644 --- a/homeassistant/components/izone/translations/ja.json +++ b/homeassistant/components/izone/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 879f4a4d4c8..8a09fd8d552 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -363,14 +363,18 @@ class JellyfinSource(MediaSource): def _media_mime_type(media_item: dict[str, Any]) -> str: """Return the mime type of a media item.""" - if not media_item[ITEM_KEY_MEDIA_SOURCES]: + if not media_item.get(ITEM_KEY_MEDIA_SOURCES): raise BrowseError("Unable to determine mime type for item without media source") media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] + + if MEDIA_SOURCE_KEY_PATH not in media_source: + raise BrowseError("Unable to determine mime type for media source without path") + path = media_source[MEDIA_SOURCE_KEY_PATH] mime_type, _ = mimetypes.guess_type(path) - if mime_type is not None: - return mime_type + if mime_type is None: + raise BrowseError(f"Unable to determine mime type for path {path}") - raise BrowseError(f"Unable to determine mime type for path {path}") + return mime_type diff --git a/homeassistant/components/jellyfin/translations/ja.json b/homeassistant/components/jellyfin/translations/ja.json index 69cc9a5279d..fe3ae792913 100644 --- a/homeassistant/components/jellyfin/translations/ja.json +++ b/homeassistant/components/jellyfin/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/jellyfin/translations/pt.json b/homeassistant/components/jellyfin/translations/pt.json index d1f8622ba3f..8e3e0f68479 100644 --- a/homeassistant/components/jellyfin/translations/pt.json +++ b/homeassistant/components/jellyfin/translations/pt.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "password": "Palavra Passe", + "password": "Palavra-passe", "url": "Endere\u00e7o", "username": "Nome de utilizador" } diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 60377d7e85b..483782c948a 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kaleidescape/__init__.py b/homeassistant/components/kaleidescape/__init__.py index a66ae25d436..f074ac640d8 100644 --- a/homeassistant/components/kaleidescape/__init__.py +++ b/homeassistant/components/kaleidescape/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kaleidescape/translations/pt.json b/homeassistant/components/kaleidescape/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index ecd9ece1bd0..68465c26c45 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/keenetic_ndms2/translations/bg.json b/homeassistant/components/keenetic_ndms2/translations/bg.json index 42c3174a4c4..4105afcbe4d 100644 --- a/homeassistant/components/keenetic_ndms2/translations/bg.json +++ b/homeassistant/components/keenetic_ndms2/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/keenetic_ndms2/translations/pt.json b/homeassistant/components/keenetic_ndms2/translations/pt.json new file mode 100644 index 00000000000..df69b1c4ff3 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index a7637879fc1..ef4e8ebb303 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) update_listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener diff --git a/homeassistant/components/kmtronic/translations/bg.json b/homeassistant/components/kmtronic/translations/bg.json index d152ddfcf20..737855f7b76 100644 --- a/homeassistant/components/kmtronic/translations/bg.json +++ b/homeassistant/components/kmtronic/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 0c34428f0a1..266eceaacee 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.21.5"], + "requirements": ["xknx==0.22.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 28cab2cea5f..92411b58312 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -48,7 +48,7 @@ "knxkeys_filename": "A f\u00e1jl a `.storage/knx/` konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r\u00e1ban helyezend\u0151.\nHome Assistant oper\u00e1ci\u00f3s rendszer eset\u00e9n ez a k\u00f6vetkez\u0151 lenne: `/config/.storage/knx/`\nP\u00e9lda: \"my_project.knxkeys\".", "knxkeys_password": "Ez a be\u00e1ll\u00edt\u00e1s a f\u00e1jl ETS-b\u0151l t\u00f6rt\u00e9n\u0151 export\u00e1l\u00e1sakor t\u00f6rt\u00e9nt." }, - "description": "K\u00e9rj\u00fck, adja meg a '.knxkeys' f\u00e1jl adatait." + "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." }, "secure_manual": { "data": { @@ -61,7 +61,7 @@ "user_id": "Ez gyakran a tunnel sz\u00e1ma +1. Teh\u00e1t a \"Tunnel 2\" felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja \"3\".", "user_password": "Jelsz\u00f3 az adott tunnelhez, amely a tunnel \u201eProperties\u201d panelj\u00e9n van be\u00e1ll\u00edtva az ETS-ben." }, - "description": "K\u00e9rj\u00fck, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat." }, "secure_tunneling": { "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index dcd44d5838f..bbac3566bca 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/knx/translations/pt.json b/homeassistant/components/knx/translations/pt.json new file mode 100644 index 00000000000..7220ef495c9 --- /dev/null +++ b/homeassistant/components/knx/translations/pt.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "tunnel": { + "data": { + "host": "Anfitri\u00e3o", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index 1ca87d9ac80..d5c5bef24fa 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -102,7 +102,7 @@ "multicast_group": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `224.0.23.12`", "multicast_port": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `3671`", "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443.\n\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f: \u043e\u0442 20 \u0434\u043e 40", - "state_updater": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0447\u0442\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438\u0437 \u0448\u0438\u043d\u044b KNX. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d, Home Assistant \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438\u0437 \u0448\u0438\u043d\u044b KNX, \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 sync_state \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u043c\u0435\u0442\u044c \u043d\u0438\u043a\u0430\u043a\u043e\u0433\u043e \u044d\u0444\u0444\u0435\u043a\u0442\u0430." + "state_updater": "\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0438\u0437 \u0448\u0438\u043d\u044b KNX. \u0415\u0441\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e, Home Assistant \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0441 \u0448\u0438\u043d\u044b KNX. \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043c\u0435\u043d\u0435\u043d \u043e\u043f\u0446\u0438\u044f\u043c\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 `sync_state`." } }, "tunnel": { diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index f5e58f3c6a1..d3c7d4da724 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_REMOVE_LISTENER: remove_stop_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json index 88855ff4d77..26e81ebd818 100644 --- a/homeassistant/components/kodi/translations/ja.json +++ b/homeassistant/components/kodi/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u7d71\u5408\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/kodi/translations/pt.json b/homeassistant/components/kodi/translations/pt.json index 441d052867f..a20906321aa 100644 --- a/homeassistant/components/kodi/translations/pt.json +++ b/homeassistant/components/kodi/translations/pt.json @@ -25,7 +25,7 @@ "data": { "host": "Servidor", "port": "Porta", - "ssl": "Conecte-se por SSL" + "ssl": "Utiliza um certificado SSL" }, "description": "Informa\u00e7\u00f5es de conex\u00e3o Kodi. Certifique-se de habilitar \"Permitir controle do Kodi via HTTP\" em Sistema / Configura\u00e7\u00f5es / Rede / Servi\u00e7os." }, diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 98440766334..620ed12ac54 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -419,7 +419,7 @@ class KonnectedView(HomeAssistantView): resp = {} if request.query.get(CONF_ZONE): resp[CONF_ZONE] = zone_num - else: + elif zone_num: resp[CONF_PIN] = ZONE_TO_PIN[zone_num] # Make sure entity is setup diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 94a58227c56..fcf94a38c18 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -6,6 +6,7 @@ import copy import logging import random import string +from typing import Any from urllib.parse import urlparse import voluptuous as vol @@ -171,11 +172,11 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 # class variable to store/share discovered host information - discovered_hosts = {} + discovered_hosts: dict[str, dict[str, Any]] = {} def __init__(self) -> None: """Initialize the Konnected flow.""" - self.data = {} + self.data: dict[str, Any] = {} self.options = OPTIONS_SCHEMA({CONF_IO: {}}) async def async_gen_config(self, host, port): @@ -271,6 +272,7 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Malformed Konnected SSDP info") else: # extract host/port from ssdp_location + assert discovery_info.ssdp_location netloc = urlparse(discovery_info.ssdp_location).netloc.split(":") self._async_abort_entries_match( {CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])} @@ -392,10 +394,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.current_opt = self.entry.options or self.entry.data[CONF_DEFAULT_OPTIONS] # as config proceeds we'll build up new options and then replace what's in the config entry - self.new_opt = {CONF_IO: {}} + self.new_opt: dict[str, dict[str, Any]] = {CONF_IO: {}} self.active_cfg = None - self.io_cfg = {} - self.current_states = [] + self.io_cfg: dict[str, Any] = {} + self.current_states: list[dict[str, Any]] = [] self.current_state = 1 @callback diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index 57cbf0d9bb9..914546e4481 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -24,7 +24,7 @@ "host": "IP c\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg a Konnected Panel csatlakoz\u00e1si adatait." + "description": "K\u00e9rem, adja meg a Konnected Panel csatlakoz\u00e1si adatait." } } }, @@ -91,7 +91,7 @@ "discovery": "V\u00e1laszoljon a h\u00e1l\u00f3zaton \u00e9rkez\u0151 felder\u00edt\u00e9si k\u00e9r\u00e9sekre", "override_api_host": "Az alap\u00e9rtelmezett Home Assistant API host-URL fel\u00fcl\u00edr\u00e1sa" }, - "description": "K\u00e9rj\u00fck, v\u00e1lassza ki a k\u00edv\u00e1nt viselked\u00e9st a panelhez", + "description": "K\u00e9rem, v\u00e1lassza ki panel k\u00edv\u00e1nt m\u0171k\u00f6d\u00e9s\u00e9t", "title": "Egy\u00e9b be\u00e1ll\u00edt\u00e1sa" }, "options_switch": { diff --git a/homeassistant/components/konnected/translations/pt.json b/homeassistant/components/konnected/translations/pt.json index 64aaf6cbf4a..2ba94342093 100644 --- a/homeassistant/components/konnected/translations/pt.json +++ b/homeassistant/components/konnected/translations/pt.json @@ -25,7 +25,7 @@ "step": { "options_binary": { "data": { - "name": "Nome (opcional)" + "name": "Nome" } }, "options_digital": { @@ -55,7 +55,7 @@ }, "options_switch": { "data": { - "name": "Nome (opcional)" + "name": "Nome" } } } diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index b431960caef..24e8ab9f0d3 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -12,7 +12,7 @@ from .helper import Plenticore _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.NUMBER] +PLATFORMS = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = plenticore - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 11bb794f799..7ae0b13f0e8 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,8 +1,6 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" -from dataclasses import dataclass from typing import NamedTuple -from homeassistant.components.number import NumberEntityDescription from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -18,7 +16,6 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) -from homeassistant.helpers.entity import EntityCategory DOMAIN = "kostal_plenticore" @@ -794,57 +791,6 @@ SENSOR_PROCESS_DATA = [ ] -@dataclass -class PlenticoreNumberEntityDescriptionMixin: - """Define an entity description mixin for number entities.""" - - module_id: str - data_id: str - fmt_from: str - fmt_to: str - - -@dataclass -class PlenticoreNumberEntityDescription( - NumberEntityDescription, PlenticoreNumberEntityDescriptionMixin -): - """Describes a Plenticore number entity.""" - - -NUMBER_SETTINGS_DATA = [ - PlenticoreNumberEntityDescription( - key="battery_min_soc", - entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=False, - icon="mdi:battery-negative", - name="Battery min SoC", - native_unit_of_measurement=PERCENTAGE, - native_max_value=100, - native_min_value=5, - native_step=5, - module_id="devices:local", - data_id="Battery:MinSoc", - fmt_from="format_round", - fmt_to="format_round_back", - ), - PlenticoreNumberEntityDescription( - key="battery_min_home_consumption", - device_class=SensorDeviceClass.POWER, - entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=False, - name="Battery min Home Consumption", - native_unit_of_measurement=POWER_WATT, - native_max_value=38000, - native_min_value=50, - native_step=1, - module_id="devices:local", - data_id="Battery:MinHomeComsumption", - fmt_from="format_round", - fmt_to="format_round_back", - ), -] - - class SwitchData(NamedTuple): """Representation of a SelectData tuple.""" diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index c87d96161a4..ee684b68974 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Callable, Iterable +from collections.abc import Callable from datetime import datetime, timedelta import logging from typing import Any @@ -122,17 +122,20 @@ class Plenticore: class DataUpdateCoordinatorMixin: """Base implementation for read and write data.""" - async def async_read_data(self, module_id: str, data_id: str) -> list[str, bool]: + _plenticore: Plenticore + name: str + + async def async_read_data( + self, module_id: str, data_id: str + ) -> dict[str, dict[str, str]] | None: """Read data from Plenticore.""" if (client := self._plenticore.client) is None: - return False + return None try: - val = await client.get_setting_values(module_id, data_id) + return await client.get_setting_values(module_id, data_id) except PlenticoreApiException: - return False - else: - return val + return None async def async_write_data(self, module_id: str, value: dict[str, str]) -> bool: """Write settings back to Plenticore.""" @@ -170,7 +173,7 @@ class PlenticoreUpdateCoordinator(DataUpdateCoordinator): update_interval=update_inverval, ) # data ids to poll - self._fetch = defaultdict(list) + self._fetch: dict[str, list[str]] = defaultdict(list) self._plenticore = plenticore def start_fetch_data(self, module_id: str, data_id: str) -> None: @@ -246,7 +249,7 @@ class PlenticoreSelectUpdateCoordinator(DataUpdateCoordinator): update_interval=update_inverval, ) # data ids to poll - self._fetch = defaultdict(list) + self._fetch: dict[str, list[str]] = defaultdict(list) self._plenticore = plenticore def start_fetch_data(self, module_id: str, data_id: str, all_options: str) -> None: @@ -284,20 +287,23 @@ class SelectDataUpdateCoordinator( async def _async_get_current_option( self, - module_id: str | dict[str, Iterable[str]], + module_id: dict[str, list[str]], ) -> dict[str, dict[str, str]]: """Get current option.""" for mid, pids in module_id.items(): all_options = pids[1] for all_option in all_options: - if all_option != "None": - val = await self.async_read_data(mid, all_option) - for option in val.values(): - if option[all_option] == "1": - fetched = {mid: {pids[0]: all_option}} - return fetched + if all_option == "None" or not ( + val := await self.async_read_data(mid, all_option) + ): + continue + for option in val.values(): + if option[all_option] == "1": + fetched = {mid: {pids[0]: all_option}} + return fetched return {mid: {pids[0]: "None"}} + return {} class PlenticoreDataFormatter: @@ -361,7 +367,7 @@ class PlenticoreDataFormatter: return "" @staticmethod - def format_float(state: str) -> int | str: + def format_float(state: str) -> float | str: """Return the given state value as float rounded to three decimal places.""" try: return round(float(state), 3) @@ -377,7 +383,7 @@ class PlenticoreDataFormatter: return state @staticmethod - def format_inverter_state(state: str) -> str: + def format_inverter_state(state: str) -> str | None: """Return a readable string of the inverter state.""" try: value = int(state) @@ -387,7 +393,7 @@ class PlenticoreDataFormatter: return PlenticoreDataFormatter.INVERTER_STATES.get(value) @staticmethod - def format_em_manager_state(state: str) -> str: + def format_em_manager_state(state: str) -> str | None: """Return a readable state of the energy manager.""" try: value = int(state) diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py index 1ad911f6d15..69fba631b34 100644 --- a/homeassistant/components/kostal_plenticore/number.py +++ b/homeassistant/components/kostal_plenticore/number.py @@ -2,25 +2,82 @@ from __future__ import annotations from abc import ABC +from dataclasses import dataclass from datetime import timedelta -from functools import partial import logging from kostal.plenticore import SettingsData -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, POWER_WATT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, NUMBER_SETTINGS_DATA, PlenticoreNumberEntityDescription +from .const import DOMAIN from .helper import PlenticoreDataFormatter, SettingDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +@dataclass +class PlenticoreNumberEntityDescriptionMixin: + """Define an entity description mixin for number entities.""" + + module_id: str + data_id: str + fmt_from: str + fmt_to: str + + +@dataclass +class PlenticoreNumberEntityDescription( + NumberEntityDescription, PlenticoreNumberEntityDescriptionMixin +): + """Describes a Plenticore number entity.""" + + +NUMBER_SETTINGS_DATA = [ + PlenticoreNumberEntityDescription( + key="battery_min_soc", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:battery-negative", + name="Battery min SoC", + native_unit_of_measurement=PERCENTAGE, + native_max_value=100, + native_min_value=5, + native_step=5, + module_id="devices:local", + data_id="Battery:MinSoc", + fmt_from="format_round", + fmt_to="format_round_back", + ), + PlenticoreNumberEntityDescription( + key="battery_min_home_consumption", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + name="Battery min Home Consumption", + native_unit_of_measurement=POWER_WATT, + native_max_value=38000, + native_min_value=50, + native_step=1, + module_id="devices:local", + data_id="Battery:MinHomeComsumption", + fmt_from="format_round", + fmt_to="format_round_back", + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -54,10 +111,9 @@ async def async_setup_entry( continue setting_data = next( - filter( - partial(lambda id, sd: id == sd.id, description.data_id), - available_settings_data[description.module_id], - ) + sd + for sd in available_settings_data[description.module_id] + if description.data_id == sd.id ) entities.append( diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 5f8fb47e85a..f66264e1d7a 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -36,7 +36,18 @@ async def async_setup_entry( timedelta(seconds=10), plenticore, ) - for module_id, data_id, name, sensor_data, fmt in SENSOR_PROCESS_DATA: + module_id: str + data_id: str + name: str + sensor_data: dict[str, Any] + fmt: str + for ( # type: ignore[assignment] + module_id, + data_id, + name, + sensor_data, + fmt, + ) in SENSOR_PROCESS_DATA: if ( module_id not in available_process_data or data_id not in available_process_data[module_id] @@ -78,7 +89,7 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): sensor_data: dict[str, Any], formatter: Callable[[str], Any], device_info: DeviceInfo, - entity_category: EntityCategory, + entity_category: EntityCategory | None, ): """Create a new Sensor Entity for Plenticore process data.""" super().__init__(coordinator) diff --git a/homeassistant/components/kostal_plenticore/translations/pt.json b/homeassistant/components/kostal_plenticore/translations/pt.json new file mode 100644 index 00000000000..df69b1c4ff3 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index bf01f27673d..db2baf56f97 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await kraken_data.async_setup() hass.data[DOMAIN] = kraken_data entry.async_on_unload(entry.add_update_listener(async_options_updated)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kraken/translations/ja.json b/homeassistant/components/kraken/translations/ja.json index 1d581131252..40a1ac53232 100644 --- a/homeassistant/components/kraken/translations/ja.json +++ b/homeassistant/components/kraken/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/kraken/translations/pt.json b/homeassistant/components/kraken/translations/pt.json new file mode 100644 index 00000000000..66ccff7f372 --- /dev/null +++ b/homeassistant/components/kraken/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 39c0d0a5b84..6c8037bdafc 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if DATA_ADDRESSES not in hass.data[DOMAIN]: hass.data[DOMAIN][DATA_ADDRESSES] = set() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kulersky/translations/ja.json b/homeassistant/components/kulersky/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/kulersky/translations/ja.json +++ b/homeassistant/components/kulersky/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/lacrosse_view/translations/de.json b/homeassistant/components/lacrosse_view/translations/de.json new file mode 100644 index 00000000000..d9aa1210fa0 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_locations": "Keine Standorte gefunden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json new file mode 100644 index 00000000000..a2a7fd23272 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "no_locations": "No locations found", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/it.json b/homeassistant/components/lacrosse_view/translations/it.json new file mode 100644 index 00000000000..9ce6c75dcbc --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "no_locations": "Nessuna localit\u00e0 trovata", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/pt-BR.json b/homeassistant/components/lacrosse_view/translations/pt-BR.json new file mode 100644 index 00000000000..29b458e5599 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_locations": "Nenhum local encontrado", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/zh-Hant.json b/homeassistant/components/lacrosse_view/translations/zh-Hant.json new file mode 100644 index 00000000000..78235452297 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_locations": "\u627e\u4e0d\u5230\u5ea7\u6a19", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/launch_library/__init__.py b/homeassistant/components/launch_library/__init__.py index 14eac4fd6a6..34ee7441351 100644 --- a/homeassistant/components/launch_library/__init__.py +++ b/homeassistant/components/launch_library/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/launch_library/translations/ja.json b/homeassistant/components/launch_library/translations/ja.json index f0e85fa9a4a..c824577578f 100644 --- a/homeassistant/components/launch_library/translations/ja.json +++ b/homeassistant/components/launch_library/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/launch_library/translations/pt.json b/homeassistant/components/launch_library/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/launch_library/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/__init__.py b/homeassistant/components/laundrify/__init__.py index 27fc412abab..4bfb84082d5 100644 --- a/homeassistant/components/laundrify/__init__.py +++ b/homeassistant/components/laundrify/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "coordinator": coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/laundrify/translations/hu.json b/homeassistant/components/laundrify/translations/hu.json index aa22ddc3da3..604bffbec52 100644 --- a/homeassistant/components/laundrify/translations/hu.json +++ b/homeassistant/components/laundrify/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "invalid_format": "\u00c9rv\u00e9nytelen form\u00e1tum. K\u00e9rj\u00fck, adja meg \u00edgy: xxx-xxx.", + "invalid_format": "\u00c9rv\u00e9nytelen form\u00e1tum. K\u00e9rem, \u00edgy adja meg: xxx-xxx.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -14,7 +14,7 @@ "data": { "code": "Hiteles\u00edt\u00e9si k\u00f3d (xxx-xxx)" }, - "description": "K\u00e9rj\u00fck, adja meg a laundrify-alkalmaz\u00e1sban megjelen\u0151 szem\u00e9lyes enged\u00e9lyez\u00e9si k\u00f3dj\u00e1t." + "description": "K\u00e9rem, adja meg a laundrify-alkalmaz\u00e1sban megjelen\u0151 szem\u00e9lyes enged\u00e9lyez\u00e9si k\u00f3dj\u00e1t." }, "reauth_confirm": { "description": "A laundrify integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie.", diff --git a/homeassistant/components/laundrify/translations/ja.json b/homeassistant/components/laundrify/translations/ja.json index 153fcf99afb..f80f610ac9a 100644 --- a/homeassistant/components/laundrify/translations/ja.json +++ b/homeassistant/components/laundrify/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -17,8 +17,8 @@ "description": "Laundrify-App\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308b\u3001\u3042\u306a\u305f\u306e\u500b\u4eba\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "reauth_confirm": { - "description": "Laundrify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Laundrify\u7d71\u5408\u3067\u306f\u3001\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index d1486fe0d32..8df579fddd7 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -120,7 +120,7 @@ async def async_setup_entry( register_lcn_address_devices(hass, config_entry) # forward config_entry to components - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) # register for LCN bus messages device_registry = dr.async_get(hass) diff --git a/homeassistant/components/lcn/translations/ca.json b/homeassistant/components/lcn/translations/ca.json index e1c08f18137..940e212a9f0 100644 --- a/homeassistant/components/lcn/translations/ca.json +++ b/homeassistant/components/lcn/translations/ca.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "codi de bloqueig rebut", "fingerprint": "codi d'empremta rebut", "send_keys": "claus d'enviament rebudes", "transmitter": "codi del transmissor rebut", diff --git a/homeassistant/components/lcn/translations/el.json b/homeassistant/components/lcn/translations/el.json index ae71f96d361..e0086482539 100644 --- a/homeassistant/components/lcn/translations/el.json +++ b/homeassistant/components/lcn/translations/el.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u03b5\u03bb\u03ae\u03c6\u03b8\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd", "fingerprint": "\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03b1\u03ba\u03c4\u03c5\u03bb\u03b9\u03ba\u03bf\u03cd \u03b1\u03c0\u03bf\u03c4\u03c5\u03c0\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", "send_keys": "\u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03ce\u03bd \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", "transmitter": "\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03bf\u03bc\u03c0\u03bf\u03cd \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", diff --git a/homeassistant/components/lcn/translations/et.json b/homeassistant/components/lcn/translations/et.json index 058873e63c5..e390ed3a2f3 100644 --- a/homeassistant/components/lcn/translations/et.json +++ b/homeassistant/components/lcn/translations/et.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "koodluku kood vastu v\u00f5etud", "fingerprint": "vastu v\u00f5etud s\u00f5rmej\u00e4ljekood", "send_keys": "vastuv\u00f5etud v\u00f5tmete saatmine", "transmitter": "saatja kood vastu v\u00f5etud", diff --git a/homeassistant/components/lcn/translations/hu.json b/homeassistant/components/lcn/translations/hu.json index 3e56a4a149b..f2b7566d838 100644 --- a/homeassistant/components/lcn/translations/hu.json +++ b/homeassistant/components/lcn/translations/hu.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "k\u00f3dz\u00e1rol\u00e1si k\u00f3d \u00e9rkezett", "fingerprint": "ujjlenyomatk\u00f3d \u00e9rkezett", "send_keys": "kulcs/gomb \u00e9rkezett", "transmitter": "t\u00e1vvez\u00e9rl\u0151 k\u00f3d \u00e9rkezett", diff --git a/homeassistant/components/lcn/translations/ja.json b/homeassistant/components/lcn/translations/ja.json index b656835dcbc..30849a56dc5 100644 --- a/homeassistant/components/lcn/translations/ja.json +++ b/homeassistant/components/lcn/translations/ja.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u30b3\u30fc\u30c9\u30ed\u30c3\u30af\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f", "fingerprint": "\u6307\u7d0b\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(fingerprint code received)", "send_keys": "\u9001\u4fe1\u30ad\u30fc\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(send keys received)", "transmitter": "\u9001\u4fe1\u6a5f\u30b3\u30fc\u30c9\u53d7\u4fe1\u3057\u307e\u3057\u305f(transmitter code received)", diff --git a/homeassistant/components/lcn/translations/no.json b/homeassistant/components/lcn/translations/no.json index 7ae5b70fe2f..cf780a06ba8 100644 --- a/homeassistant/components/lcn/translations/no.json +++ b/homeassistant/components/lcn/translations/no.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "kodel\u00e5skode mottatt", "fingerprint": "fingeravtrykkkode mottatt", "send_keys": "sende n\u00f8kler mottatt", "transmitter": "senderkode mottatt", diff --git a/homeassistant/components/lcn/translations/pl.json b/homeassistant/components/lcn/translations/pl.json index 18446607167..2a2415d4138 100644 --- a/homeassistant/components/lcn/translations/pl.json +++ b/homeassistant/components/lcn/translations/pl.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "otrzymano kod blokady", "fingerprint": "otrzymano kod odcisku palca", "send_keys": "otrzymano klucze wysy\u0142ania", "transmitter": "otrzymano kod nadajnika", diff --git a/homeassistant/components/lcn/translations/ru.json b/homeassistant/components/lcn/translations/ru.json index 0953ee96ef7..7f198d6571f 100644 --- a/homeassistant/components/lcn/translations/ru.json +++ b/homeassistant/components/lcn/translations/ru.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438", "fingerprint": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u043e\u0442\u043f\u0435\u0447\u0430\u0442\u043a\u0430 \u043f\u0430\u043b\u044c\u0446\u0430", "send_keys": "\u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u043a\u043b\u044e\u0447\u0435\u0439", "transmitter": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u0447\u0438\u043a\u0430", diff --git a/homeassistant/components/lcn/translations/tr.json b/homeassistant/components/lcn/translations/tr.json index 2c2d94611d0..f3a40e165c6 100644 --- a/homeassistant/components/lcn/translations/tr.json +++ b/homeassistant/components/lcn/translations/tr.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "kod kilit kodu al\u0131nd\u0131", "fingerprint": "al\u0131nan parmak izi kodu", "send_keys": "al\u0131nan anahtarlar\u0131 g\u00f6nder", "transmitter": "verici kodu al\u0131nd\u0131", diff --git a/homeassistant/components/lcn/translations/zh-Hant.json b/homeassistant/components/lcn/translations/zh-Hant.json index fe80da6694f..e6b104596b0 100644 --- a/homeassistant/components/lcn/translations/zh-Hant.json +++ b/homeassistant/components/lcn/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u5df2\u6536\u5230\u9396\u5b9a\u78bc", "fingerprint": "\u5df2\u6536\u5230\u6307\u7d0b\u78bc", "send_keys": "\u5df2\u6536\u5230\u50b3\u9001\u91d1\u9470", "transmitter": "\u5df2\u6536\u5230\u767c\u5c04\u5668\u78bc", diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index f8f6fcf26fd..941042d5bce 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -47,7 +47,6 @@ class LGDevice(MediaPlayerEntity): self._port = port self._attr_unique_id = unique_id - self._name = None self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -94,8 +93,6 @@ class LGDevice(MediaPlayerEntity): elif response["msg"] == "SPK_LIST_VIEW_INFO": if "i_vol" in data: self._volume = data["i_vol"] - if "s_user_name" in data: - self._name = data["s_user_name"] if "i_vol_min" in data: self._volume_min = data["i_vol_min"] if "i_vol_max" in data: diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json index ef7bf32a051..52d57eda809 100644 --- a/homeassistant/components/lg_soundbar/strings.json +++ b/homeassistant/components/lg_soundbar/strings.json @@ -11,8 +11,7 @@ "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%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/lg_soundbar/translations/ar.json b/homeassistant/components/lg_soundbar/translations/ar.json new file mode 100644 index 00000000000..3fc833f41f1 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062e\u062f\u0645\u0629 \u062a\u0645 \u062a\u0647\u064a\u0623\u062a\u0647\u0627 \u0645\u0633\u0628\u0642\u0627", + "existing_instance_updated": "\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u062d\u0627\u0644\u064a" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ca.json b/homeassistant/components/lg_soundbar/translations/ca.json new file mode 100644 index 00000000000..8c445361eb8 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/de.json b/homeassistant/components/lg_soundbar/translations/de.json new file mode 100644 index 00000000000..a840fb04abe --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/el.json b/homeassistant/components/lg_soundbar/translations/el.json new file mode 100644 index 00000000000..7fa31f8fe9d --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "existing_instance_updated": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json index a646279203f..10441d21536 100644 --- a/homeassistant/components/lg_soundbar/translations/en.json +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is already configured", - "existing_instance_updated": "Updated existing configuration." + "already_configured": "Device is already configured" }, "error": { "cannot_connect": "Failed to connect" diff --git a/homeassistant/components/lg_soundbar/translations/et.json b/homeassistant/components/lg_soundbar/translations/et.json new file mode 100644 index 00000000000..227250382c0 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "existing_instance_updated": "V\u00e4rskendati olemasolevat konfiguratsiooni." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/fr.json b/homeassistant/components/lg_soundbar/translations/fr.json new file mode 100644 index 00000000000..b13f3d0d595 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/hu.json b/homeassistant/components/lg_soundbar/translations/hu.json new file mode 100644 index 00000000000..c39033d273a --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "existing_instance_updated": "A megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 friss\u00edtve." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/id.json b/homeassistant/components/lg_soundbar/translations/id.json new file mode 100644 index 00000000000..74d380f3a1a --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "existing_instance_updated": "Memperbarui konfigurasi yang ada." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/it.json b/homeassistant/components/lg_soundbar/translations/it.json new file mode 100644 index 00000000000..c9f58f15aa2 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "existing_instance_updated": "Configurazione esistente aggiornata." + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ja.json b/homeassistant/components/lg_soundbar/translations/ja.json new file mode 100644 index 00000000000..cd40cd88044 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "existing_instance_updated": "\u65e2\u5b58\u306e\u69cb\u6210\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/nl.json b/homeassistant/components/lg_soundbar/translations/nl.json new file mode 100644 index 00000000000..7345479d97a --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "existing_instance_updated": "Bestaande configuratie bijgewerkt." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/no.json b/homeassistant/components/lg_soundbar/translations/no.json new file mode 100644 index 00000000000..41eef0e4c16 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "existing_instance_updated": "Oppdatert eksisterende konfigurasjon." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/pl.json b/homeassistant/components/lg_soundbar/translations/pl.json new file mode 100644 index 00000000000..1b84366cfa4 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/pt-BR.json b/homeassistant/components/lg_soundbar/translations/pt-BR.json new file mode 100644 index 00000000000..dfbff8cddc8 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/pt.json b/homeassistant/components/lg_soundbar/translations/pt.json new file mode 100644 index 00000000000..91ff56e73f8 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ru.json b/homeassistant/components/lg_soundbar/translations/ru.json new file mode 100644 index 00000000000..f7961eb2e7e --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/tr.json b/homeassistant/components/lg_soundbar/translations/tr.json new file mode 100644 index 00000000000..5eb581847fb --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "existing_instance_updated": "Mevcut yap\u0131land\u0131rma g\u00fcncellendi." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/zh-Hant.json b/homeassistant/components/lg_soundbar/translations/zh-Hant.json new file mode 100644 index 00000000000..8680a863901 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ar.json b/homeassistant/components/life360/translations/ar.json new file mode 100644 index 00000000000..32dbd371473 --- /dev/null +++ b/homeassistant/components/life360/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062d\u0633\u0627\u0628 \u062a\u0645 \u0625\u0639\u062f\u0627\u062f\u0647 \u0645\u0633\u0628\u0642\u0627", + "reauth_successful": "\u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0648\u062b\u064a\u0642 \u062a\u0645\u062a \u0628\u0646\u062c\u0627\u062d" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + } + }, + "options": { + "step": { + "init": { + "title": "\u062e\u064a\u0627\u0631\u0627\u062a \u0627\u0644\u062d\u0633\u0627\u0628" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ca.json b/homeassistant/components/life360/translations/ca.json index 875692a661a..f6b7a081863 100644 --- a/homeassistant/components/life360/translations/ca.json +++ b/homeassistant/components/life360/translations/ca.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "El compte ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_username": "Nom d'usuari incorrecte", "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", "username": "Nom d'usuari" }, "description": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url}). Pot ser que ho hagis de fer abans d'afegir cap compte.", - "title": "Informaci\u00f3 del compte Life360" + "title": "Configuraci\u00f3 del compte Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostra la conducci\u00f3 com a estat", + "driving_speed": "Velocitat de conducci\u00f3", + "limit_gps_acc": "Limita la precisi\u00f3 del GPS", + "max_gps_accuracy": "Precisi\u00f3 m\u00e0xima del GPS (metres)", + "set_drive_speed": "Configura el llindar de velocitat de conducci\u00f3" + }, + "title": "Opcions del compte" } } } diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 516b0255349..9e6e819a179 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_username": "Ung\u00fcltiger Benutzername", "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", "username": "Benutzername" }, "description": "Erweiterte Optionen sind in der [Life360-Dokumentation]({docs_url}) zu finden.\nDies sollte vor dem Hinzuf\u00fcgen von Kontoinformationen getan werden.", - "title": "Life360-Kontoinformationen" + "title": "Life360-Konto konfigurieren" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Fahren als Zustand anzeigen", + "driving_speed": "Fahrgeschwindigkeit", + "limit_gps_acc": "GPS-Genauigkeit einschr\u00e4nken", + "max_gps_accuracy": "Maximale GPS-Genauigkeit (Meter)", + "set_drive_speed": "Schwellenwert f\u00fcr die Fahrgeschwindigkeit festlegen" + }, + "title": "Kontoeinstellungen" } } } diff --git a/homeassistant/components/life360/translations/el.json b/homeassistant/components/life360/translations/el.json index 07106d89d63..f0db8e10ed6 100644 --- a/homeassistant/components/life360/translations/el.json +++ b/homeassistant/components/life360/translations/el.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "invalid_username": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", @@ -23,5 +32,19 @@ "title": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Life360" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03bf\u03b4\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c9\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", + "driving_speed": "\u03a4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 \u03bf\u03b4\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2", + "limit_gps_acc": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1\u03c2 GPS", + "max_gps_accuracy": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 GPS (\u03bc\u03ad\u03c4\u03c1\u03b1)", + "set_drive_speed": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03c1\u03b9\u03bf \u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bf\u03b4\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/en.json b/homeassistant/components/life360/translations/en.json index b4c9eb452f6..4e7ff35c814 100644 --- a/homeassistant/components/life360/translations/en.json +++ b/homeassistant/components/life360/translations/en.json @@ -1,44 +1,50 @@ { - "config": { - "step": { - "user": { - "title": "Configure Life360 Account", - "data": { - "username": "Username", - "password": "Password" + "config": { + "abort": { + "already_configured": "Account is already configured", + "invalid_auth": "Invalid authentication", + "reauth_successful": "Re-authentication was successful", + "unknown": "Unexpected error" + }, + "create_entry": { + "default": "To set advanced options, see [Life360 documentation]({docs_url})." + }, + "error": { + "already_configured": "Account is already configured", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_username": "Invalid username", + "unknown": "Unexpected error" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "To set advanced options, see [Life360 documentation]({docs_url}).\nYou may want to do that before adding accounts.", + "title": "Configure Life360 Account" + } } - }, - "reauth_confirm": { - "title": "Reauthenticate Integration", - "data": { - "password": "Password" - } - } }, - "error": { - "invalid_auth": "Invalid authentication", - "already_configured": "Account is already configured", - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" - }, - "abort": { - "invalid_auth": "Invalid authentication", - "already_configured": "Account is already configured", - "reauth_successful": "Re-authentication was successful" - } - }, - "options": { - "step": { - "init": { - "title": "Account Options", - "data": { - "limit_gps_acc": "Limit GPS accuracy", - "max_gps_accuracy": "Max GPS accuracy (meters)", - "set_drive_speed": "Set driving speed threshold", - "driving_speed": "Driving speed", - "driving": "Show driving as state" + "options": { + "step": { + "init": { + "data": { + "driving": "Show driving as state", + "driving_speed": "Driving speed", + "limit_gps_acc": "Limit GPS accuracy", + "max_gps_accuracy": "Max GPS accuracy (meters)", + "set_drive_speed": "Set driving speed threshold" + }, + "title": "Account Options" + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/et.json b/homeassistant/components/life360/translations/et.json index d9cbbbb30f5..360aa8275f7 100644 --- a/homeassistant/components/life360/translations/et.json +++ b/homeassistant/components/life360/translations/et.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", "invalid_auth": "Tuvastamise viga", + "reauth_successful": "Taastuvastamine \u00f5nnestus", "unknown": "Ootamatu t\u00f5rge" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Kasutaja on juba seadistatud", + "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamise viga", "invalid_username": "Vale kasutajanimi", "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", "username": "Kasutajanimi" }, "description": "T\u00e4psemate suvandite kohta leiad teemat [Life360 documentation]({docs_url}).\nTee seda enne uute kontode lisamist.", - "title": "Life360 konto teave" + "title": "Seadista Life360 konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Kuva s\u00f5itmist olekuna", + "driving_speed": "S\u00f5idukiirus", + "limit_gps_acc": "GPS-i t\u00e4psuse piiramine", + "max_gps_accuracy": "GPS-i maksimaalne t\u00e4psus (meetrites)", + "set_drive_speed": "M\u00e4\u00e4ra s\u00f5idukiiruse l\u00e4vi" + }, + "title": "Konto suvandid" } } } diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json index f58b789b267..ce1fd3f7757 100644 --- a/homeassistant/components/life360/translations/fr.json +++ b/homeassistant/components/life360/translations/fr.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "invalid_auth": "Authentification non valide", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "unknown": "Erreur inattendue" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification non valide", "invalid_username": "Nom d'utilisateur non valide", "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" }, "description": "Pour d\u00e9finir des options avanc\u00e9es, voir [Documentation Life360]({docs_url}).\nVous pouvez le faire avant d'ajouter des comptes.", - "title": "Informations sur le compte Life360" + "title": "Configuration du compte Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Afficher la conduite comme \u00e9tat", + "driving_speed": "Vitesse de conduite", + "limit_gps_acc": "Limiter la pr\u00e9cision du GPS", + "max_gps_accuracy": "Pr\u00e9cision maximale du GPS (en m\u00e8tres)", + "set_drive_speed": "D\u00e9finition du seuil de vitesse de conduite" + }, + "title": "Options de compte" } } } diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 5dbd2898971..33f615d00c7 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", @@ -23,5 +32,19 @@ "title": "Life360 fi\u00f3kadatok" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Vezet\u00e9s megjelen\u00edt\u00e9se \u00e1llapotk\u00e9nt", + "driving_speed": "Menetsebess\u00e9g", + "limit_gps_acc": "A GPS pontoss\u00e1g\u00e1nak korl\u00e1toz\u00e1sa", + "max_gps_accuracy": "Maxim\u00e1lis GPS pontoss\u00e1g (m\u00e9ter)", + "set_drive_speed": "Vezet\u00e9si sebess\u00e9g k\u00fcsz\u00f6b\u00e9rt\u00e9k\u00e9nek be\u00e1ll\u00edt\u00e1sa" + }, + "title": "Fi\u00f3kbe\u00e1ll\u00edt\u00e1sok" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/id.json b/homeassistant/components/life360/translations/id.json index 21a93366c44..dfcdf97f46f 100644 --- a/homeassistant/components/life360/translations/id.json +++ b/homeassistant/components/life360/translations/id.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Akun sudah dikonfigurasi", "invalid_auth": "Autentikasi tidak valid", + "reauth_successful": "Autentikasi ulang berhasil", "unknown": "Kesalahan yang tidak diharapkan" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Akun sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "invalid_username": "Nama pengguna tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", "username": "Nama Pengguna" }, "description": "Untuk mengatur opsi tingkat lanjut, baca [dokumentasi Life360]({docs_url}).\nAnda mungkin ingin melakukannya sebelum menambahkan akun.", - "title": "Info Akun Life360" + "title": "Konfigurasikan Akun Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Tampilkan mengemudi sebagai status", + "driving_speed": "Kecepatan mengemudi", + "limit_gps_acc": "Batasi akurasi GPS", + "max_gps_accuracy": "Akurasi GPS maksimum (meter)", + "set_drive_speed": "Tetapkan ambang batas kecepatan mengemudi" + }, + "title": "Opsi Akun" } } } diff --git a/homeassistant/components/life360/translations/it.json b/homeassistant/components/life360/translations/it.json index 04d88e75378..4f139301274 100644 --- a/homeassistant/components/life360/translations/it.json +++ b/homeassistant/components/life360/translations/it.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", "invalid_auth": "Autenticazione non valida", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "unknown": "Errore imprevisto" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "invalid_username": "Nome utente non valido", "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", "username": "Nome utente" }, "description": "Per impostare le opzioni avanzate, vedere [Documentazione di Life360]({docs_url}).\n\u00c8 consigliabile eseguire questa operazione prima di aggiungere gli account.", - "title": "Informazioni sull'account Life360" + "title": "Configura l'account Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostra la guida come stato", + "driving_speed": "Velocit\u00e0 di guida", + "limit_gps_acc": "Limita la precisione del GPS", + "max_gps_accuracy": "Precisione GPS massima (metri)", + "set_drive_speed": "Impostare la soglia di velocit\u00e0 di guida" + }, + "title": "Opzioni dell'account" } } } diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index 772b44b31d8..ab320748086 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_username": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", @@ -23,5 +32,19 @@ "title": "Life360\u30a2\u30ab\u30a6\u30f3\u30c8\u60c5\u5831" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u30c9\u30e9\u30a4\u30d3\u30f3\u30b0\u3092\u72b6\u614b\u3068\u3057\u3066\u8868\u793a\u3059\u308b", + "driving_speed": "\u8d70\u884c\u901f\u5ea6", + "limit_gps_acc": "GPS\u306e\u7cbe\u5ea6\u3092\u5236\u9650\u3059\u308b", + "max_gps_accuracy": "GPS\u306e\u6700\u5927\u7cbe\u5ea6(\u30e1\u30fc\u30c8\u30eb)", + "set_drive_speed": "\u8d70\u884c\u901f\u5ea6\u306e\u3057\u304d\u3044\u5024\u3092\u8a2d\u5b9a\u3059\u308b" + }, + "title": "\u30a2\u30ab\u30a6\u30f3\u30c8\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index 612b0d5c4f7..b0e54bde3c5 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Account is al geconfigureerd", "invalid_auth": "Ongeldige authenticatie", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "Account is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "invalid_username": "Ongeldige gebruikersnaam", "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "title": "Integratie herauthenticeren" + }, "user": { "data": { "password": "Wachtwoord", @@ -23,5 +32,12 @@ "title": "Life360-accountgegevens" } } + }, + "options": { + "step": { + "init": { + "title": "Accountinstellingen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/no.json b/homeassistant/components/life360/translations/no.json index 9a95a976657..7213a665607 100644 --- a/homeassistant/components/life360/translations/no.json +++ b/homeassistant/components/life360/translations/no.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "invalid_auth": "Ugyldig godkjenning", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Kontoen er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "invalid_username": "Ugyldig brukernavn", "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", "username": "Brukernavn" }, "description": "For \u00e5 angi avanserte alternativer, se [Life360 dokumentasjon]({docs_url}). \nDet kan hende du vil gj\u00f8re det f\u00f8r du legger til kontoer.", - "title": "Life360 Kontoinformasjon" + "title": "Konfigurer Life360-konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Vis kj\u00f8ring som tilstand", + "driving_speed": "Kj\u00f8rehastighet", + "limit_gps_acc": "Begrens GPS-n\u00f8yaktigheten", + "max_gps_accuracy": "Maksimal GPS-n\u00f8yaktighet (meter)", + "set_drive_speed": "Still inn kj\u00f8rehastighetsterskel" + }, + "title": "Kontoalternativer" } } } diff --git a/homeassistant/components/life360/translations/pl.json b/homeassistant/components/life360/translations/pl.json index 30ba7ddc5b6..2b1c7138079 100644 --- a/homeassistant/components/life360/translations/pl.json +++ b/homeassistant/components/life360/translations/pl.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", "invalid_auth": "Niepoprawne uwierzytelnienie", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Konto jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, "description": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url}). Mo\u017cesz to zrobi\u0107 przed dodaniem kont.", - "title": "Informacje o koncie Life360" + "title": "Konfiguracja konta Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Poka\u017c dane drogowe jako stan", + "driving_speed": "Pr\u0119dko\u015b\u0107 jazdy", + "limit_gps_acc": "Ogranicz dok\u0142adno\u015b\u0107 GPS", + "max_gps_accuracy": "Maksymalna dok\u0142adno\u015b\u0107 GPS (w metrach)", + "set_drive_speed": "Ustaw pr\u00f3g dla pr\u0119dko\u015bci jazdy" + }, + "title": "Opcje konta" } } } diff --git a/homeassistant/components/life360/translations/pt-BR.json b/homeassistant/components/life360/translations/pt-BR.json index 7753c0f84dc..13349bcef67 100644 --- a/homeassistant/components/life360/translations/pt-BR.json +++ b/homeassistant/components/life360/translations/pt-BR.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "Integra\u00e7\u00e3o Reautenticar", "unknown": "Erro inesperado" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "A conta j\u00e1 foi configurada", + "cannot_connect": "Falhou ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Senha", "username": "Usu\u00e1rio" }, "description": "Para definir op\u00e7\u00f5es avan\u00e7adas, consulte [Documenta\u00e7\u00e3o da Life360] ({docs_url}). \n Voc\u00ea pode querer fazer isso antes de adicionar contas.", - "title": "Informa\u00e7\u00f5es da conta Life360" + "title": "Configurar conta Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostrar condu\u00e7\u00e3o como estado", + "driving_speed": "Velocidade de condu\u00e7\u00e3o", + "limit_gps_acc": "Limitar a precis\u00e3o do GPS", + "max_gps_accuracy": "Precis\u00e3o m\u00e1xima do GPS (metros)", + "set_drive_speed": "Definir limite de velocidade de condu\u00e7\u00e3o" + }, + "title": "Op\u00e7\u00f5es da conta" } } } diff --git a/homeassistant/components/life360/translations/pt.json b/homeassistant/components/life360/translations/pt.json index 71370e40068..cc3b190458f 100644 --- a/homeassistant/components/life360/translations/pt.json +++ b/homeassistant/components/life360/translations/pt.json @@ -1,16 +1,25 @@ { "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "unknown": "Erro inesperado" }, "error": { "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_username": "Nome de utilizador incorreto", "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index 4b3fbceb5d6..c0cd51a72d4 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", - "title": "Life360" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u043a\u0430\u043a \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", + "driving_speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u0432\u043e\u0436\u0434\u0435\u043d\u0438\u044f", + "limit_gps_acc": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0442\u044c \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c GPS", + "max_gps_accuracy": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c GPS (\u0432 \u043c\u0435\u0442\u0440\u0430\u0445)", + "set_drive_speed": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u043e\u0440\u043e\u0433 \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430" } } } diff --git a/homeassistant/components/life360/translations/tr.json b/homeassistant/components/life360/translations/tr.json index 1304151d948..52b083b83fd 100644 --- a/homeassistant/components/life360/translations/tr.json +++ b/homeassistant/components/life360/translations/tr.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_username": "Ge\u00e7ersiz kullan\u0131c\u0131 ad\u0131", "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Geli\u015fmi\u015f se\u00e7enekleri ayarlamak i\u00e7in [Life360 belgelerine]( {docs_url} ) bak\u0131n.\n Bunu hesap eklemeden \u00f6nce yapmak isteyebilirsiniz.", - "title": "Life360 Hesap Bilgileri" + "title": "Life360 Hesab\u0131n\u0131 Yap\u0131land\u0131r" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "S\u00fcr\u00fc\u015f\u00fc durum olarak g\u00f6ster", + "driving_speed": "S\u00fcr\u00fc\u015f h\u0131z\u0131", + "limit_gps_acc": "GPS do\u011frulu\u011funu s\u0131n\u0131rlay\u0131n", + "max_gps_accuracy": "Maksimum GPS do\u011frulu\u011fu (metre)", + "set_drive_speed": "S\u00fcr\u00fc\u015f h\u0131z\u0131 e\u015fi\u011fini ayarla" + }, + "title": "Hesap Se\u00e7enekleri" } } } diff --git a/homeassistant/components/life360/translations/zh-Hant.json b/homeassistant/components/life360/translations/zh-Hant.json index ad7fde2e21d..55e55bb30c7 100644 --- a/homeassistant/components/life360/translations/zh-Hant.json +++ b/homeassistant/components/life360/translations/zh-Hant.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_username": "\u4f7f\u7528\u8005\u540d\u7a31\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u6b32\u8a2d\u5b9a\u9032\u968e\u9078\u9805\uff0c\u8acb\u53c3\u95b1 [Life360 \u6587\u4ef6]({docs_url})\u3002\n\u5efa\u8b70\u65bc\u65b0\u589e\u5e33\u865f\u524d\uff0c\u5148\u9032\u884c\u4e86\u89e3\u3002", - "title": "Life360 \u5e33\u865f\u8cc7\u8a0a" + "title": "\u8a2d\u5b9a Life360 \u5e33\u865f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u5c07\u884c\u99db\u4e2d\u986f\u793a\u70ba\u72c0\u614b", + "driving_speed": "\u884c\u99db\u901f\u5ea6", + "limit_gps_acc": "\u9650\u5236 GPS \u7cbe\u51c6\u5ea6", + "max_gps_accuracy": "\u6700\u9ad8 GPS \u7cbe\u78ba\u5ea6\uff08\u516c\u5c3a\uff09", + "set_drive_speed": "\u8a2d\u5b9a\u901f\u9650\u503c" + }, + "title": "\u5e33\u865f\u9078\u9805" } } } diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index df672c07143..3faf69483d5 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -1,19 +1,41 @@ """Support for LIFX.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +from datetime import datetime, timedelta +import socket +from typing import Any + +from aiolifx.aiolifx import Light +from aiolifx_connection import LIFXConnection import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STARTED, + Platform, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN, TARGET_ANY +from .coordinator import LIFXUpdateCoordinator +from .discovery import async_discover_devices, async_trigger_discovery +from .manager import LIFXManager +from .migration import async_migrate_entities_devices, async_migrate_legacy_entries +from .util import async_entry_is_legacy, async_get_legacy_entry CONF_SERVER = "server" CONF_BROADCAST = "broadcast" + INTERFACE_SCHEMA = vol.Schema( { vol.Optional(CONF_SERVER): cv.string, @@ -22,39 +44,176 @@ INTERFACE_SCHEMA = vol.Schema( } ) -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: {LIGHT_DOMAIN: vol.Schema(vol.All(cv.ensure_list, [INTERFACE_SCHEMA]))}}, - extra=vol.ALLOW_EXTRA, +CONFIG_SCHEMA = vol.All( + cv.deprecated(DOMAIN), + vol.Schema( + { + DOMAIN: { + LIGHT_DOMAIN: vol.Schema(vol.All(cv.ensure_list, [INTERFACE_SCHEMA])) + } + }, + extra=vol.ALLOW_EXTRA, + ), ) -DATA_LIFX_MANAGER = "lifx_manager" PLATFORMS = [Platform.LIGHT] +DISCOVERY_INTERVAL = timedelta(minutes=15) +MIGRATION_INTERVAL = timedelta(minutes=5) + +DISCOVERY_COOLDOWN = 5 + + +async def async_legacy_migration( + hass: HomeAssistant, + legacy_entry: ConfigEntry, + discovered_devices: Iterable[Light], +) -> bool: + """Migrate config entries.""" + existing_serials = { + entry.unique_id + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.unique_id and not async_entry_is_legacy(entry) + } + # device.mac_addr is not the mac_address, its the serial number + hosts_by_serial = {device.mac_addr: device.ip_addr for device in discovered_devices} + missing_discovery_count = async_migrate_legacy_entries( + hass, hosts_by_serial, existing_serials, legacy_entry + ) + if missing_discovery_count: + _LOGGER.info( + "Migration in progress, waiting to discover %s device(s)", + missing_discovery_count, + ) + return False + + _LOGGER.debug( + "Migration successful, removing legacy entry %s", legacy_entry.entry_id + ) + await hass.config_entries.async_remove(legacy_entry.entry_id) + return True + + +class LIFXDiscoveryManager: + """Manage discovery and migration.""" + + def __init__(self, hass: HomeAssistant, migrating: bool) -> None: + """Init the manager.""" + self.hass = hass + self.lock = asyncio.Lock() + self.migrating = migrating + self._cancel_discovery: CALLBACK_TYPE | None = None + + @callback + def async_setup_discovery_interval(self) -> None: + """Set up discovery at an interval.""" + if self._cancel_discovery: + self._cancel_discovery() + self._cancel_discovery = None + discovery_interval = ( + MIGRATION_INTERVAL if self.migrating else DISCOVERY_INTERVAL + ) + _LOGGER.debug( + "LIFX starting discovery with interval: %s and migrating: %s", + discovery_interval, + self.migrating, + ) + self._cancel_discovery = async_track_time_interval( + self.hass, self.async_discovery, discovery_interval + ) + + async def async_discovery(self, *_: Any) -> None: + """Discovery and migrate LIFX devics.""" + migrating_was_in_progress = self.migrating + + async with self.lock: + discovered = await async_discover_devices(self.hass) + + if legacy_entry := async_get_legacy_entry(self.hass): + migration_complete = await async_legacy_migration( + self.hass, legacy_entry, discovered + ) + if migration_complete and migrating_was_in_progress: + self.migrating = False + _LOGGER.debug( + "LIFX migration complete, switching to normal discovery interval: %s", + DISCOVERY_INTERVAL, + ) + self.async_setup_discovery_interval() + + if discovered: + async_trigger_discovery(self.hass, discovered) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LIFX component.""" - conf = config.get(DOMAIN) + hass.data[DOMAIN] = {} + migrating = bool(async_get_legacy_entry(hass)) + discovery_manager = LIFXDiscoveryManager(hass, migrating) - hass.data[DOMAIN] = conf or {} + @callback + def _async_delayed_discovery(now: datetime) -> None: + """Start an untracked task to discover devices. - if conf is not None: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - ) + We do not want the discovery task to block startup. + """ + asyncio.create_task(discovery_manager.async_discovery()) + + # Let the system settle a bit before starting discovery + # to reduce the risk we miss devices because the event + # loop is blocked at startup. + discovery_manager.async_setup_discovery_interval() + async_call_later(hass, DISCOVERY_COOLDOWN, _async_delayed_discovery) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, discovery_manager.async_discovery + ) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LIFX from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + if async_entry_is_legacy(entry): + return True + + if legacy_entry := async_get_legacy_entry(hass): + # If the legacy entry still exists, harvest the entities + # that are moving to this config entry. + async_migrate_entities_devices(hass, legacy_entry.entry_id, entry) + + assert entry.unique_id is not None + domain_data = hass.data[DOMAIN] + if DATA_LIFX_MANAGER not in domain_data: + manager = LIFXManager(hass) + domain_data[DATA_LIFX_MANAGER] = manager + manager.async_setup() + + host = entry.data[CONF_HOST] + connection = LIFXConnection(host, TARGET_ANY) + try: + await connection.async_setup() + except socket.gaierror as ex: + raise ConfigEntryNotReady(f"Could not resolve {host}: {ex}") from ex + coordinator = LIFXUpdateCoordinator(hass, connection, entry.title) + coordinator.async_setup() + await coordinator.async_config_entry_first_refresh() + + domain_data[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - hass.data.pop(DATA_LIFX_MANAGER).cleanup() - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if async_entry_is_legacy(entry): + return True + domain_data = hass.data[DOMAIN] + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: LIFXUpdateCoordinator = domain_data.pop(entry.entry_id) + coordinator.connection.async_stop() + # Only the DATA_LIFX_MANAGER left, remove it. + if len(domain_data) == 1: + manager: LIFXManager = domain_data.pop(DATA_LIFX_MANAGER) + manager.async_unload() + return unload_ok diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index c48bee9e4e7..30b42e640f8 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -1,16 +1,240 @@ """Config flow flow LIFX.""" -import aiolifx +from __future__ import annotations -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_flow +import asyncio +import socket +from typing import Any -from .const import DOMAIN +from aiolifx.aiolifx import Light +from aiolifx_connection import LIFXConnection +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.components.dhcp import DhcpServiceInfo +from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import DiscoveryInfoType + +from .const import _LOGGER, CONF_SERIAL, DOMAIN, TARGET_ANY +from .discovery import async_discover_devices +from .util import ( + async_entry_is_legacy, + async_execute_lifx, + async_get_legacy_entry, + formatted_serial, + lifx_features, + mac_matches_serial_number, +) -async def _async_has_devices(hass: HomeAssistant) -> bool: - """Return if there are devices that can be discovered.""" - lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() - return len(lifx_ip_addresses) > 0 +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for tplink.""" + VERSION = 1 -config_entry_flow.register_discovery_flow(DOMAIN, "LIFX", _async_has_devices) + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_devices: dict[str, Light] = {} + self._discovered_device: Light | None = None + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + mac = discovery_info.macaddress + host = discovery_info.ip + hass = self.hass + for entry in self._async_current_entries(): + if ( + entry.unique_id + and not async_entry_is_legacy(entry) + and mac_matches_serial_number(mac, entry.unique_id) + ): + if entry.data[CONF_HOST] != host: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_HOST: host} + ) + hass.async_create_task( + hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + return await self._async_handle_discovery(host) + + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle HomeKit discovery.""" + return await self._async_handle_discovery(host=discovery_info.host) + + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle discovery.""" + _LOGGER.debug("async_step_integration_discovery %s", discovery_info) + serial = discovery_info[CONF_SERIAL] + host = discovery_info[CONF_HOST] + await self.async_set_unique_id(formatted_serial(serial)) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return await self._async_handle_discovery(host, serial) + + async def _async_handle_discovery( + self, host: str, serial: str | None = None + ) -> FlowResult: + """Handle any discovery.""" + _LOGGER.debug("Discovery %s %s", host, serial) + self._async_abort_entries_match({CONF_HOST: host}) + self.context[CONF_HOST] = host + if any( + progress.get("context", {}).get(CONF_HOST) == host + for progress in self._async_in_progress() + ): + return self.async_abort(reason="already_in_progress") + if not ( + device := await self._async_try_connect( + host, serial=serial, raise_on_progress=True + ) + ): + return self.async_abort(reason="cannot_connect") + self._discovered_device = device + return await self.async_step_discovery_confirm() + + @callback + def _async_discovered_pending_migration(self) -> bool: + """Check if a discovered device is pending migration.""" + assert self.unique_id is not None + if not (legacy_entry := async_get_legacy_entry(self.hass)): + return False + device_registry = dr.async_get(self.hass) + existing_device = device_registry.async_get_device( + identifiers={(DOMAIN, self.unique_id)} + ) + return bool( + existing_device is not None + and legacy_entry.entry_id in existing_device.config_entries + ) + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + _LOGGER.debug( + "Confirming discovery: %s with serial %s", + self._discovered_device.label, + self.unique_id, + ) + if user_input is not None or self._async_discovered_pending_migration(): + return self._async_create_entry_from_device(self._discovered_device) + + self._set_confirm_only() + placeholders = { + "label": self._discovered_device.label, + "host": self._discovered_device.ip_addr, + "serial": self.unique_id, + } + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + host = user_input[CONF_HOST] + if not host: + return await self.async_step_pick_device() + if ( + device := await self._async_try_connect(host, raise_on_progress=False) + ) is None: + errors["base"] = "cannot_connect" + else: + return self._async_create_entry_from_device(device) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}), + errors=errors, + ) + + async def async_step_pick_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the step to pick discovered device.""" + if user_input is not None: + serial = user_input[CONF_DEVICE] + await self.async_set_unique_id(serial, raise_on_progress=False) + device_without_label = self._discovered_devices[serial] + device = await self._async_try_connect( + device_without_label.ip_addr, raise_on_progress=False + ) + if not device: + return self.async_abort(reason="cannot_connect") + return self._async_create_entry_from_device(device) + + configured_serials: set[str] = set() + configured_hosts: set[str] = set() + for entry in self._async_current_entries(): + if entry.unique_id and not async_entry_is_legacy(entry): + configured_serials.add(entry.unique_id) + configured_hosts.add(entry.data[CONF_HOST]) + self._discovered_devices = { + # device.mac_addr is not the mac_address, its the serial number + device.mac_addr: device + for device in await async_discover_devices(self.hass) + } + devices_name = { + serial: f"{serial} ({device.ip_addr})" + for serial, device in self._discovered_devices.items() + if serial not in configured_serials + and device.ip_addr not in configured_hosts + } + # Check if there is at least one device + if not devices_name: + return self.async_abort(reason="no_devices_found") + return self.async_show_form( + step_id="pick_device", + data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), + ) + + @callback + def _async_create_entry_from_device(self, device: Light) -> FlowResult: + """Create a config entry from a smart device.""" + self._abort_if_unique_id_configured(updates={CONF_HOST: device.ip_addr}) + return self.async_create_entry( + title=device.label, + data={CONF_HOST: device.ip_addr}, + ) + + async def _async_try_connect( + self, host: str, serial: str | None = None, raise_on_progress: bool = True + ) -> Light | None: + """Try to connect.""" + self._async_abort_entries_match({CONF_HOST: host}) + connection = LIFXConnection(host, TARGET_ANY) + try: + await connection.async_setup() + except socket.gaierror: + return None + device: Light = connection.device + device.get_hostfirmware() + try: + message = await async_execute_lifx(device.get_color) + except asyncio.TimeoutError: + return None + finally: + connection.async_stop() + if ( + lifx_features(device)["relays"] is True + or device.host_firmware_version is None + ): + return None # relays not supported + # device.mac_addr is not the mac_address, its the serial number + device.mac_addr = serial or message.target_addr + await self.async_set_unique_id( + formatted_serial(device.mac_addr), raise_on_progress=raise_on_progress + ) + return device diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index 8628527c428..ec756c2091f 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -1,3 +1,19 @@ """Const for LIFX.""" +import logging + DOMAIN = "lifx" + +TARGET_ANY = "00:00:00:00:00:00" + +DISCOVERY_INTERVAL = 10 +MESSAGE_TIMEOUT = 1.65 +MESSAGE_RETRIES = 5 +OVERALL_TIMEOUT = 9 +UNAVAILABLE_GRACE = 90 + +CONF_SERIAL = "serial" + +DATA_LIFX_MANAGER = "lifx_manager" + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py new file mode 100644 index 00000000000..87ba46e94d1 --- /dev/null +++ b/homeassistant/components/lifx/coordinator.py @@ -0,0 +1,158 @@ +"""Coordinator for lifx.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +from functools import partial +from typing import cast + +from aiolifx.aiolifx import Light +from aiolifx_connection import LIFXConnection + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + _LOGGER, + MESSAGE_RETRIES, + MESSAGE_TIMEOUT, + TARGET_ANY, + UNAVAILABLE_GRACE, +) +from .util import async_execute_lifx, get_real_mac_addr, lifx_features + +REQUEST_REFRESH_DELAY = 0.35 + + +class LIFXUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator to gather data for a specific lifx device.""" + + def __init__( + self, + hass: HomeAssistant, + connection: LIFXConnection, + title: str, + ) -> None: + """Initialize DataUpdateCoordinator.""" + assert connection.device is not None + self.connection = connection + self.device: Light = connection.device + self.lock = asyncio.Lock() + update_interval = timedelta(seconds=10) + super().__init__( + hass, + _LOGGER, + name=f"{title} ({self.device.ip_addr})", + update_interval=update_interval, + # We don't want an immediate refresh since the device + # takes a moment to reflect the state change + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), + ) + + @callback + def async_setup(self) -> None: + """Change timeouts.""" + self.device.timeout = MESSAGE_TIMEOUT + self.device.retry_count = MESSAGE_RETRIES + self.device.unregister_timeout = UNAVAILABLE_GRACE + + @property + def serial_number(self) -> str: + """Return the internal mac address.""" + return cast( + str, self.device.mac_addr + ) # device.mac_addr is not the mac_address, its the serial number + + @property + def mac_address(self) -> str: + """Return the physical mac address.""" + return get_real_mac_addr( + # device.mac_addr is not the mac_address, its the serial number + self.device.mac_addr, + self.device.host_firmware_version, + ) + + async def _async_update_data(self) -> None: + """Fetch all device data from the api.""" + async with self.lock: + if self.device.host_firmware_version is None: + self.device.get_hostfirmware() + if self.device.product is None: + self.device.get_version() + try: + response = await async_execute_lifx(self.device.get_color) + except asyncio.TimeoutError as ex: + raise UpdateFailed( + f"Failed to fetch state from device: {self.device.ip_addr}" + ) from ex + if self.device.product is None: + raise UpdateFailed( + f"Failed to fetch get version from device: {self.device.ip_addr}" + ) + # device.mac_addr is not the mac_address, its the serial number + if self.device.mac_addr == TARGET_ANY: + self.device.mac_addr = response.target_addr + if lifx_features(self.device)["multizone"]: + try: + await self.async_update_color_zones() + except asyncio.TimeoutError as ex: + raise UpdateFailed( + f"Failed to fetch zones from device: {self.device.ip_addr}" + ) from ex + + async def async_update_color_zones(self) -> None: + """Get updated color information for each zone.""" + zone = 0 + top = 1 + while zone < top: + # Each get_color_zones can update 8 zones at once + resp = await async_execute_lifx( + partial(self.device.get_color_zones, start_index=zone) + ) + zone += 8 + top = resp.count + + # We only await multizone responses so don't ask for just one + if zone == top - 1: + zone -= 1 + + async def async_get_color(self) -> None: + """Send a get color message to the device.""" + await async_execute_lifx(self.device.get_color) + + async def async_set_power(self, state: bool, duration: int | None) -> None: + """Send a set power message to the device.""" + await async_execute_lifx( + partial(self.device.set_power, state, duration=duration) + ) + + async def async_set_color( + self, hsbk: list[float | int | None], duration: int | None + ) -> None: + """Send a set color message to the device.""" + await async_execute_lifx( + partial(self.device.set_color, hsbk, duration=duration) + ) + + async def async_set_color_zones( + self, + start_index: int, + end_index: int, + hsbk: list[float | int | None], + duration: int | None, + apply: int, + ) -> None: + """Send a set color zones message to the device.""" + await async_execute_lifx( + partial( + self.device.set_color_zones, + start_index=start_index, + end_index=end_index, + color=hsbk, + duration=duration, + apply=apply, + ) + ) diff --git a/homeassistant/components/lifx/discovery.py b/homeassistant/components/lifx/discovery.py new file mode 100644 index 00000000000..1c6e9ab3060 --- /dev/null +++ b/homeassistant/components/lifx/discovery.py @@ -0,0 +1,58 @@ +"""The lifx integration discovery.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable + +from aiolifx.aiolifx import LifxDiscovery, Light, ScanManager + +from homeassistant import config_entries +from homeassistant.components import network +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant, callback + +from .const import CONF_SERIAL, DOMAIN + +DEFAULT_TIMEOUT = 8.5 + + +async def async_discover_devices(hass: HomeAssistant) -> Iterable[Light]: + """Discover lifx devices.""" + all_lights: dict[str, Light] = {} + broadcast_addrs = await network.async_get_ipv4_broadcast_addresses(hass) + discoveries = [] + for address in broadcast_addrs: + manager = ScanManager(str(address)) + lifx_discovery = LifxDiscovery(hass.loop, manager, broadcast_ip=str(address)) + discoveries.append(lifx_discovery) + lifx_discovery.start() + + await asyncio.sleep(DEFAULT_TIMEOUT) + for discovery in discoveries: + all_lights.update(discovery.lights) + discovery.cleanup() + + return all_lights.values() + + +@callback +def async_init_discovery_flow(hass: HomeAssistant, host: str, serial: str) -> None: + """Start discovery of devices.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_HOST: host, CONF_SERIAL: serial}, + ) + ) + + +@callback +def async_trigger_discovery( + hass: HomeAssistant, + discovered_devices: Iterable[Light], +) -> None: + """Trigger config flows for discovered devices.""" + for device in discovered_devices: + # device.mac_addr is not the mac_address, its the serial number + async_init_discovery_flow(hass, device.ip_addr, device.mac_addr) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 28390e5c02a..28a678d5e8f 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -2,86 +2,56 @@ from __future__ import annotations import asyncio -from dataclasses import dataclass -from datetime import timedelta -from functools import partial -from ipaddress import IPv4Address -import logging +from datetime import datetime, timedelta import math +from typing import Any -import aiolifx as aiolifx_module -from aiolifx.aiolifx import LifxDiscovery, Light +from aiolifx import products import aiolifx_effects as aiolifx_effects_module -from awesomeversion import AwesomeVersion import voluptuous as vol from homeassistant import util -from homeassistant.components import network from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_HS_COLOR, - ATTR_KELVIN, - ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_XY_COLOR, - COLOR_GROUP, - DOMAIN, LIGHT_TURN_ON_SCHEMA, - VALID_BRIGHTNESS, - VALID_BRIGHTNESS_PCT, ColorMode, LightEntity, LightEntityFeature, - preprocess_turn_on_alternatives, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_MODE, - ATTR_MODEL, - ATTR_SW_VERSION, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODEL, ATTR_SW_VERSION +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback, EntityPlatform -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.color as color_util -from . import ( - CONF_BROADCAST, - CONF_PORT, - CONF_SERVER, - DATA_LIFX_MANAGER, - DOMAIN as LIFX_DOMAIN, +from .const import DATA_LIFX_MANAGER, DOMAIN +from .coordinator import LIFXUpdateCoordinator +from .manager import ( + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + LIFXManager, ) - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=10) - -DISCOVERY_INTERVAL = 10 -MESSAGE_TIMEOUT = 1 -MESSAGE_RETRIES = 8 -UNAVAILABLE_GRACE = 90 - -FIX_MAC_FW = AwesomeVersion("3.70") +from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk SERVICE_LIFX_SET_STATE = "set_state" +COLOR_ZONE_POPULATE_DELAY = 0.3 + ATTR_INFRARED = "infrared" ATTR_ZONES = "zones" ATTR_POWER = "power" +SERVICE_LIFX_SET_STATE = "set_state" + LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( { **LIGHT_TURN_ON_SCHEMA, @@ -91,645 +61,152 @@ LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( } ) -SERVICE_EFFECT_PULSE = "effect_pulse" -SERVICE_EFFECT_COLORLOOP = "effect_colorloop" -SERVICE_EFFECT_STOP = "effect_stop" - -ATTR_POWER_ON = "power_on" -ATTR_PERIOD = "period" -ATTR_CYCLES = "cycles" -ATTR_SPREAD = "spread" -ATTR_CHANGE = "change" - -PULSE_MODE_BLINK = "blink" -PULSE_MODE_BREATHE = "breathe" -PULSE_MODE_PING = "ping" -PULSE_MODE_STROBE = "strobe" -PULSE_MODE_SOLID = "solid" - -PULSE_MODES = [ - PULSE_MODE_BLINK, - PULSE_MODE_BREATHE, - PULSE_MODE_PING, - PULSE_MODE_STROBE, - PULSE_MODE_SOLID, -] - -LIFX_EFFECT_SCHEMA = { - vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, -} - -LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( - { - **LIFX_EFFECT_SCHEMA, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, - vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) - ), - vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) - ), - vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( - vol.Coerce(tuple), - vol.ExactSequence( - ( - vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), - ) - ), - ), - vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, - ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), - ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), - ATTR_MODE: vol.In(PULSE_MODES), - } -) - -LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema( - { - **LIFX_EFFECT_SCHEMA, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), - ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), - ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), - ATTR_TRANSITION: cv.positive_float, - } -) - -LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({}) - - -def aiolifx(): - """Return the aiolifx module.""" - return aiolifx_module - - -def aiolifx_effects(): - """Return the aiolifx_effects module.""" - return aiolifx_effects_module - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the LIFX light platform. Obsolete.""" - _LOGGER.warning("LIFX no longer works with light platform configuration") +HSBK_HUE = 0 +HSBK_SATURATION = 1 +HSBK_BRIGHTNESS = 2 +HSBK_KELVIN = 3 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up LIFX from a config entry.""" - # Priority 1: manual config - if not (interfaces := hass.data[LIFX_DOMAIN].get(DOMAIN)): - # Priority 2: Home Assistant enabled interfaces - ip_addresses = ( - source_ip - for source_ip in await network.async_get_enabled_source_ips(hass) - if isinstance(source_ip, IPv4Address) and not source_ip.is_loopback - ) - interfaces = [{CONF_SERVER: str(ip)} for ip in ip_addresses] - + domain_data = hass.data[DOMAIN] + coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id] + manager: LIFXManager = domain_data[DATA_LIFX_MANAGER] + device = coordinator.device platform = entity_platform.async_get_current_platform() - lifx_manager = LIFXManager(hass, platform, config_entry, async_add_entities) - hass.data[DATA_LIFX_MANAGER] = lifx_manager - - for interface in interfaces: - lifx_manager.start_discovery(interface) - - -def lifx_features(bulb): - """Return a feature map for this bulb, or a default map if unknown.""" - return aiolifx().products.features_map.get( - bulb.product - ) or aiolifx().products.features_map.get(1) - - -def find_hsbk(hass, **kwargs): - """Find the desired color from a number of possible inputs.""" - hue, saturation, brightness, kelvin = [None] * 4 - - preprocess_turn_on_alternatives(hass, kwargs) - - if ATTR_HS_COLOR in kwargs: - hue, saturation = kwargs[ATTR_HS_COLOR] - elif ATTR_RGB_COLOR in kwargs: - hue, saturation = color_util.color_RGB_to_hs(*kwargs[ATTR_RGB_COLOR]) - elif ATTR_XY_COLOR in kwargs: - hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) - - if hue is not None: - hue = int(hue / 360 * 65535) - saturation = int(saturation / 100 * 65535) - kelvin = 3500 - - if ATTR_COLOR_TEMP in kwargs: - kelvin = int( - color_util.color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - ) - saturation = 0 - - if ATTR_BRIGHTNESS in kwargs: - brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) - - hsbk = [hue, saturation, brightness, kelvin] - return None if hsbk == [None] * 4 else hsbk - - -def merge_hsbk(base, change): - """Copy change on top of base, except when None.""" - if change is None: - return None - return [b if c is None else c for b, c in zip(base, change)] - - -@dataclass -class InFlightDiscovery: - """Represent a LIFX device that is being discovered.""" - - device: Light - lock: asyncio.Lock - - -class LIFXManager: - """Representation of all known LIFX entities.""" - - def __init__( - self, - hass: HomeAssistant, - platform: EntityPlatform, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - ) -> None: - """Initialize the light.""" - self.entities: dict[str, LIFXLight] = {} - self.switch_devices: list[str] = [] - self.hass = hass - self.platform = platform - self.config_entry = config_entry - self.async_add_entities = async_add_entities - self.effects_conductor = aiolifx_effects().Conductor(hass.loop) - self.discoveries: list[LifxDiscovery] = [] - self.discoveries_inflight: dict[str, InFlightDiscovery] = {} - self.cleanup_unsub = self.hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, self.cleanup - ) - self.entity_registry_updated_unsub = self.hass.bus.async_listen( - er.EVENT_ENTITY_REGISTRY_UPDATED, self.entity_registry_updated - ) - - self.register_set_state() - self.register_effects() - - def start_discovery(self, interface): - """Start discovery on a network interface.""" - kwargs = {"discovery_interval": DISCOVERY_INTERVAL} - if broadcast_ip := interface.get(CONF_BROADCAST): - kwargs["broadcast_ip"] = broadcast_ip - lifx_discovery = aiolifx().LifxDiscovery(self.hass.loop, self, **kwargs) - - kwargs = {} - if listen_ip := interface.get(CONF_SERVER): - kwargs["listen_ip"] = listen_ip - if listen_port := interface.get(CONF_PORT): - kwargs["listen_port"] = listen_port - lifx_discovery.start(**kwargs) - - self.discoveries.append(lifx_discovery) - - @callback - def cleanup(self, event=None): - """Release resources.""" - self.cleanup_unsub() - self.entity_registry_updated_unsub() - - for discovery in self.discoveries: - discovery.cleanup() - - for service in ( - SERVICE_LIFX_SET_STATE, - SERVICE_EFFECT_STOP, - SERVICE_EFFECT_PULSE, - SERVICE_EFFECT_COLORLOOP, - ): - self.hass.services.async_remove(LIFX_DOMAIN, service) - - def register_set_state(self): - """Register the LIFX set_state service call.""" - self.platform.async_register_entity_service( - SERVICE_LIFX_SET_STATE, LIFX_SET_STATE_SCHEMA, "set_state" - ) - - def register_effects(self): - """Register the LIFX effects as hass service calls.""" - - async def service_handler(service: ServiceCall) -> None: - """Apply a service, i.e. start an effect.""" - entities = await self.platform.async_extract_from_service(service) - if entities: - await self.start_effect(entities, service.service, **service.data) - - self.hass.services.async_register( - LIFX_DOMAIN, - SERVICE_EFFECT_PULSE, - service_handler, - schema=LIFX_EFFECT_PULSE_SCHEMA, - ) - - self.hass.services.async_register( - LIFX_DOMAIN, - SERVICE_EFFECT_COLORLOOP, - service_handler, - schema=LIFX_EFFECT_COLORLOOP_SCHEMA, - ) - - self.hass.services.async_register( - LIFX_DOMAIN, - SERVICE_EFFECT_STOP, - service_handler, - schema=LIFX_EFFECT_STOP_SCHEMA, - ) - - async def start_effect(self, entities, service, **kwargs): - """Start a light effect on entities.""" - bulbs = [light.bulb for light in entities] - - if service == SERVICE_EFFECT_PULSE: - effect = aiolifx_effects().EffectPulse( - power_on=kwargs.get(ATTR_POWER_ON), - period=kwargs.get(ATTR_PERIOD), - cycles=kwargs.get(ATTR_CYCLES), - mode=kwargs.get(ATTR_MODE), - hsbk=find_hsbk(self.hass, **kwargs), - ) - await self.effects_conductor.start(effect, bulbs) - elif service == SERVICE_EFFECT_COLORLOOP: - preprocess_turn_on_alternatives(self.hass, kwargs) - - brightness = None - if ATTR_BRIGHTNESS in kwargs: - brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) - - effect = aiolifx_effects().EffectColorloop( - power_on=kwargs.get(ATTR_POWER_ON), - period=kwargs.get(ATTR_PERIOD), - change=kwargs.get(ATTR_CHANGE), - spread=kwargs.get(ATTR_SPREAD), - transition=kwargs.get(ATTR_TRANSITION), - brightness=brightness, - ) - await self.effects_conductor.start(effect, bulbs) - elif service == SERVICE_EFFECT_STOP: - await self.effects_conductor.stop(bulbs) - - def clear_inflight_discovery(self, inflight: InFlightDiscovery) -> None: - """Clear in-flight discovery.""" - self.discoveries_inflight.pop(inflight.device.mac_addr, None) - - @callback - def register(self, bulb: Light) -> None: - """Allow a single in-flight discovery per bulb.""" - if bulb.mac_addr in self.switch_devices: - _LOGGER.debug( - "Skipping discovered LIFX Switch at %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - return - - # Try to bail out of discovery as early as possible - if bulb.mac_addr in self.entities: - entity = self.entities[bulb.mac_addr] - entity.registered = True - _LOGGER.debug("Reconnected to %s", entity.who) - return - - if bulb.mac_addr not in self.discoveries_inflight: - inflight = InFlightDiscovery(bulb, asyncio.Lock()) - self.discoveries_inflight[bulb.mac_addr] = inflight - _LOGGER.debug( - "First discovery response received from %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - else: - _LOGGER.debug( - "Duplicate discovery response received from %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - - self.hass.async_create_task( - self._async_handle_discovery(self.discoveries_inflight[bulb.mac_addr]) - ) - - async def _async_handle_discovery(self, inflight: InFlightDiscovery) -> None: - """Handle LIFX bulb registration lifecycle.""" - - # only allow a single discovery process per discovered device - async with inflight.lock: - - # Bail out if an entity was created by a previous discovery while - # this discovery was waiting for the asyncio lock to release. - if inflight.device.mac_addr in self.entities: - self.clear_inflight_discovery(inflight) - entity: LIFXLight = self.entities[inflight.device.mac_addr] - entity.registered = True - _LOGGER.debug("Reconnected to %s", entity.who) - return - - # Determine the product info so that LIFX Switches - # can be skipped. - ack = AwaitAioLIFX().wait - - if inflight.device.product is None: - if await ack(inflight.device.get_version) is None: - _LOGGER.debug( - "Failed to discover product information for %s (%s)", - inflight.device.ip_addr, - inflight.device.mac_addr, - ) - self.clear_inflight_discovery(inflight) - return - - if lifx_features(inflight.device)["relays"] is True: - _LOGGER.debug( - "Skipping discovered LIFX Switch at %s (%s)", - inflight.device.ip_addr, - inflight.device.mac_addr, - ) - self.switch_devices.append(inflight.device.mac_addr) - self.clear_inflight_discovery(inflight) - return - - await self._async_process_discovery(inflight=inflight) - - async def _async_process_discovery(self, inflight: InFlightDiscovery) -> None: - """Process discovery of a device.""" - bulb = inflight.device - ack = AwaitAioLIFX().wait - - bulb.timeout = MESSAGE_TIMEOUT - bulb.retry_count = MESSAGE_RETRIES - bulb.unregister_timeout = UNAVAILABLE_GRACE - - # Read initial state - if bulb.color is None: - if await ack(bulb.get_color) is None: - _LOGGER.debug( - "Failed to determine current state of %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - self.clear_inflight_discovery(inflight) - return - - if lifx_features(bulb)["multizone"]: - entity: LIFXLight = LIFXStrip(bulb.mac_addr, bulb, self.effects_conductor) - elif lifx_features(bulb)["color"]: - entity = LIFXColor(bulb.mac_addr, bulb, self.effects_conductor) - else: - entity = LIFXWhite(bulb.mac_addr, bulb, self.effects_conductor) - - self.entities[bulb.mac_addr] = entity - self.async_add_entities([entity], True) - _LOGGER.debug("Entity created for %s", entity.who) - self.clear_inflight_discovery(inflight) - - @callback - def unregister(self, bulb: Light) -> None: - """Mark unresponsive bulbs as unavailable in Home Assistant.""" - if bulb.mac_addr in self.entities: - entity = self.entities[bulb.mac_addr] - entity.registered = False - entity.async_write_ha_state() - _LOGGER.debug("Disconnected from %s", entity.who) - - @callback - def entity_registry_updated(self, event): - """Handle entity registry updated.""" - if event.data["action"] == "remove": - self.remove_empty_devices() - - def remove_empty_devices(self): - """Remove devices with no entities.""" - entity_reg = er.async_get(self.hass) - device_reg = dr.async_get(self.hass) - device_list = dr.async_entries_for_config_entry( - device_reg, self.config_entry.entry_id - ) - for device_entry in device_list: - if not er.async_entries_for_device( - entity_reg, - device_entry.id, - include_disabled_entities=True, - ): - device_reg.async_update_device( - device_entry.id, remove_config_entry_id=self.config_entry.entry_id - ) - - -class AwaitAioLIFX: - """Wait for an aiolifx callback and return the message.""" - - def __init__(self): - """Initialize the wrapper.""" - self.message = None - self.event = asyncio.Event() - - @callback - def callback(self, bulb, message): - """Handle responses.""" - self.message = message - self.event.set() - - async def wait(self, method): - """Call an aiolifx method and wait for its response.""" - self.message = None - self.event.clear() - method(callb=self.callback) - - await self.event.wait() - return self.message - - -def convert_8_to_16(value): - """Scale an 8 bit level into 16 bits.""" - return (value << 8) | value - - -def convert_16_to_8(value): - """Scale a 16 bit level into 8 bits.""" - return value >> 8 - - -class LIFXLight(LightEntity): + platform.async_register_entity_service( + SERVICE_LIFX_SET_STATE, + LIFX_SET_STATE_SCHEMA, + "set_state", + ) + if lifx_features(device)["multizone"]: + entity: LIFXLight = LIFXStrip(coordinator, manager, entry) + elif lifx_features(device)["color"]: + entity = LIFXColor(coordinator, manager, entry) + else: + entity = LIFXWhite(coordinator, manager, entry) + async_add_entities([entity]) + + +class LIFXLight(CoordinatorEntity[LIFXUpdateCoordinator], LightEntity): """Representation of a LIFX light.""" _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT def __init__( self, - mac_addr: str, - bulb: Light, - effects_conductor: aiolifx_effects_module.Conductor, + coordinator: LIFXUpdateCoordinator, + manager: LIFXManager, + entry: ConfigEntry, ) -> None: """Initialize the light.""" - self.mac_addr = mac_addr + super().__init__(coordinator) + bulb = coordinator.device + self.mac_addr = bulb.mac_addr self.bulb = bulb - self.effects_conductor = effects_conductor - self.registered = True - self.postponed_update = None - self.lock = asyncio.Lock() - - def get_mac_addr(self): - """Increment the last byte of the mac address by one for FW>3.70.""" - if ( - self.bulb.host_firmware_version - and AwesomeVersion(self.bulb.host_firmware_version) >= FIX_MAC_FW - ): - octets = [int(octet, 16) for octet in self.mac_addr.split(":")] - octets[5] = (octets[5] + 1) % 256 - return ":".join(f"{octet:02x}" for octet in octets) - return self.mac_addr - - @property - def device_info(self) -> DeviceInfo: - """Return information about the device.""" - _map = aiolifx().products.product_map - + bulb_features = lifx_features(bulb) + self.manager = manager + self.effects_conductor: aiolifx_effects_module.Conductor = ( + manager.effects_conductor + ) + self.postponed_update: CALLBACK_TYPE | None = None + self.entry = entry + self._attr_unique_id = self.coordinator.serial_number + self._attr_name = bulb.label + self._attr_min_mireds = math.floor( + color_util.color_temperature_kelvin_to_mired(bulb_features["max_kelvin"]) + ) + self._attr_max_mireds = math.ceil( + color_util.color_temperature_kelvin_to_mired(bulb_features["min_kelvin"]) + ) info = DeviceInfo( - identifiers={(LIFX_DOMAIN, self.unique_id)}, - connections={(dr.CONNECTION_NETWORK_MAC, self.get_mac_addr())}, + identifiers={(DOMAIN, coordinator.serial_number)}, + connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)}, manufacturer="LIFX", name=self.name, ) - - if (model := (_map.get(self.bulb.product) or self.bulb.product)) is not None: + _map = products.product_map + if (model := (_map.get(bulb.product) or bulb.product)) is not None: info[ATTR_MODEL] = str(model) - if (version := self.bulb.host_firmware_version) is not None: + if (version := bulb.host_firmware_version) is not None: info[ATTR_SW_VERSION] = version - - return info - - @property - def available(self): - """Return the availability of the bulb.""" - return self.registered - - @property - def unique_id(self): - """Return a unique ID.""" - return self.mac_addr - - @property - def name(self): - """Return the name of the bulb.""" - return self.bulb.label - - @property - def who(self): - """Return a string identifying the bulb by name and mac.""" - return f"{self.name} ({self.mac_addr})" - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - kelvin = lifx_features(self.bulb)["max_kelvin"] - return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin)) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - kelvin = lifx_features(self.bulb)["min_kelvin"] - return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin)) - - @property - def color_mode(self) -> ColorMode: - """Return the color mode of the light.""" - bulb_features = lifx_features(self.bulb) + self._attr_device_info = info if bulb_features["min_kelvin"] != bulb_features["max_kelvin"]: - return ColorMode.COLOR_TEMP - return ColorMode.BRIGHTNESS + color_mode = ColorMode.COLOR_TEMP + else: + color_mode = ColorMode.BRIGHTNESS + self._attr_color_mode = color_mode + self._attr_supported_color_modes = {color_mode} @property - def supported_color_modes(self) -> set[ColorMode]: - """Flag supported color modes.""" - return {self.color_mode} - - @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" fade = self.bulb.power_level / 65535 - return convert_16_to_8(int(fade * self.bulb.color[2])) + return convert_16_to_8(int(fade * self.bulb.color[HSBK_BRIGHTNESS])) @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the color temperature.""" - _, sat, _, kelvin = self.bulb.color - if sat: - return None - return color_util.color_temperature_kelvin_to_mired(kelvin) + return color_util.color_temperature_kelvin_to_mired( + self.bulb.color[HSBK_KELVIN] + ) @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" - return self.bulb.power_level != 0 + return bool(self.bulb.power_level != 0) @property - def effect(self): + def effect(self) -> str | None: """Return the name of the currently running effect.""" - effect = self.effects_conductor.effect(self.bulb) - if effect: - return f"lifx_effect_{effect.name}" + if effect := self.effects_conductor.effect(self.bulb): + return f"effect_{effect.name}" return None - async def update_hass(self, now=None): - """Request new status and push it to hass.""" - self.postponed_update = None - await self.async_update() - self.async_write_ha_state() - - async def update_during_transition(self, when): + async def update_during_transition(self, when: int) -> None: """Update state at the start and end of a transition.""" if self.postponed_update: self.postponed_update() + self.postponed_update = None # Transition has started - await self.update_hass() + self.async_write_ha_state() + + # The state reply we get back may be stale so we also request + # a refresh to get a fresh state + # https://lan.developer.lifx.com/docs/changing-a-device + await self.coordinator.async_request_refresh() # Transition has ended if when > 0: + + async def _async_refresh(now: datetime) -> None: + """Refresh the state.""" + await self.coordinator.async_refresh() + self.postponed_update = async_track_point_in_utc_time( self.hass, - self.update_hass, + _async_refresh, util.dt.utcnow() + timedelta(milliseconds=when), ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - kwargs[ATTR_POWER] = True - self.hass.async_create_task(self.set_state(**kwargs)) + await self.set_state(**{**kwargs, ATTR_POWER: True}) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - kwargs[ATTR_POWER] = False - self.hass.async_create_task(self.set_state(**kwargs)) + await self.set_state(**{**kwargs, ATTR_POWER: False}) - async def set_state(self, **kwargs): + async def set_state(self, **kwargs: Any) -> None: """Set a color on the light and turn it on/off.""" - async with self.lock: + self.coordinator.async_set_updated_data(None) + async with self.coordinator.lock: + # Cancel any pending refreshes bulb = self.bulb await self.effects_conductor.stop([bulb]) @@ -752,89 +229,113 @@ class LIFXLight(LightEntity): hsbk = find_hsbk(self.hass, **kwargs) - # Send messages, waiting for ACK each time - ack = AwaitAioLIFX().wait - if not self.is_on: if power_off: - await self.set_power(ack, False) + await self.set_power(False) # If fading on with color, set color immediately if hsbk and power_on: - await self.set_color(ack, hsbk, kwargs) - await self.set_power(ack, True, duration=fade) + await self.set_color(hsbk, kwargs) + await self.set_power(True, duration=fade) elif hsbk: - await self.set_color(ack, hsbk, kwargs, duration=fade) + await self.set_color(hsbk, kwargs, duration=fade) elif power_on: - await self.set_power(ack, True, duration=fade) + await self.set_power(True, duration=fade) else: - if power_on: - await self.set_power(ack, True) if hsbk: - await self.set_color(ack, hsbk, kwargs, duration=fade) + await self.set_color(hsbk, kwargs, duration=fade) + # The response from set_color will tell us if the + # bulb is actually on or not, so we don't need to + # call power_on if its already on + if power_on and self.bulb.power_level == 0: + await self.set_power(True) + elif power_on: + await self.set_power(True) if power_off: - await self.set_power(ack, False, duration=fade) - - # Avoid state ping-pong by holding off updates as the state settles - await asyncio.sleep(0.3) + await self.set_power(False, duration=fade) # Update when the transition starts and ends await self.update_during_transition(fade) - async def set_power(self, ack, pwr, duration=0): + async def set_power( + self, + pwr: bool, + duration: int = 0, + ) -> None: """Send a power change to the bulb.""" - await ack(partial(self.bulb.set_power, pwr, duration=duration)) + try: + await self.coordinator.async_set_power(pwr, duration) + except asyncio.TimeoutError as ex: + raise HomeAssistantError(f"Timeout setting power for {self.name}") from ex - async def set_color(self, ack, hsbk, kwargs, duration=0): + async def set_color( + self, + hsbk: list[float | int | None], + kwargs: dict[str, Any], + duration: int = 0, + ) -> None: """Send a color change to the bulb.""" - hsbk = merge_hsbk(self.bulb.color, hsbk) - await ack(partial(self.bulb.set_color, hsbk, duration=duration)) + merged_hsbk = merge_hsbk(self.bulb.color, hsbk) + try: + await self.coordinator.async_set_color(merged_hsbk, duration) + except asyncio.TimeoutError as ex: + raise HomeAssistantError(f"Timeout setting color for {self.name}") from ex - async def default_effect(self, **kwargs): + async def get_color( + self, + ) -> None: + """Send a get color message to the bulb.""" + try: + await self.coordinator.async_get_color() + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting getting color for {self.name}" + ) from ex + + async def default_effect(self, **kwargs: Any) -> None: """Start an effect with default parameters.""" - service = kwargs[ATTR_EFFECT] - data = {ATTR_ENTITY_ID: self.entity_id} await self.hass.services.async_call( - LIFX_DOMAIN, service, data, context=self._context + DOMAIN, + kwargs[ATTR_EFFECT], + {ATTR_ENTITY_ID: self.entity_id}, + context=self._context, ) - async def async_update(self): - """Update bulb status.""" - if self.available and not self.lock.locked(): - await AwaitAioLIFX().wait(self.bulb.get_color) + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + self.manager.async_register_entity(self.entity_id, self.entry.entry_id) + ) + return await super().async_added_to_hass() class LIFXWhite(LIFXLight): """Representation of a white-only LIFX light.""" - @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return [SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP] + _attr_effect_list = [SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP] class LIFXColor(LIFXLight): """Representation of a color LIFX light.""" - @property - def color_mode(self) -> ColorMode: - """Return the color mode of the light.""" - sat = self.bulb.color[1] - if sat: - return ColorMode.HS - return ColorMode.COLOR_TEMP + _attr_effect_list = [ + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + ] @property def supported_color_modes(self) -> set[ColorMode]: - """Flag supported color modes.""" + """Return the supported color modes.""" return {ColorMode.COLOR_TEMP, ColorMode.HS} @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return [SERVICE_EFFECT_COLORLOOP, SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP] + def color_mode(self) -> ColorMode: + """Return the color mode of the light.""" + has_sat = self.bulb.color[HSBK_SATURATION] + return ColorMode.HS if has_sat else ColorMode.COLOR_TEMP @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the hs value.""" hue, sat, _, _ = self.bulb.color hue = hue / 65535 * 360 @@ -845,63 +346,70 @@ class LIFXColor(LIFXLight): class LIFXStrip(LIFXColor): """Representation of a LIFX light strip with multiple zones.""" - async def set_color(self, ack, hsbk, kwargs, duration=0): + async def set_color( + self, + hsbk: list[float | int | None], + kwargs: dict[str, Any], + duration: int = 0, + ) -> None: """Send a color change to the bulb.""" bulb = self.bulb - num_zones = len(bulb.color_zones) + color_zones = bulb.color_zones + num_zones = len(color_zones) + + # Zone brightness is not reported when powered off + if not self.is_on and hsbk[HSBK_BRIGHTNESS] is None: + await self.set_power(True) + await asyncio.sleep(COLOR_ZONE_POPULATE_DELAY) + await self.update_color_zones() + await self.set_power(False) if (zones := kwargs.get(ATTR_ZONES)) is None: # Fast track: setting all zones to the same brightness and color # can be treated as a single-zone bulb. - if hsbk[2] is not None and hsbk[3] is not None: - await super().set_color(ack, hsbk, kwargs, duration) + first_zone = color_zones[0] + first_zone_brightness = first_zone[HSBK_BRIGHTNESS] + all_zones_have_same_brightness = all( + color_zones[zone][HSBK_BRIGHTNESS] == first_zone_brightness + for zone in range(num_zones) + ) + all_zones_are_the_same = all( + color_zones[zone] == first_zone for zone in range(num_zones) + ) + if ( + all_zones_have_same_brightness or hsbk[HSBK_BRIGHTNESS] is not None + ) and (all_zones_are_the_same or hsbk[HSBK_KELVIN] is not None): + await super().set_color(hsbk, kwargs, duration) return zones = list(range(0, num_zones)) else: zones = [x for x in set(zones) if x < num_zones] - # Zone brightness is not reported when powered off - if not self.is_on and hsbk[2] is None: - await self.set_power(ack, True) - await asyncio.sleep(0.3) - await self.update_color_zones() - await self.set_power(ack, False) - await asyncio.sleep(0.3) - # Send new color to each zone for index, zone in enumerate(zones): - zone_hsbk = merge_hsbk(bulb.color_zones[zone], hsbk) + zone_hsbk = merge_hsbk(color_zones[zone], hsbk) apply = 1 if (index == len(zones) - 1) else 0 - set_zone = partial( - bulb.set_color_zones, - start_index=zone, - end_index=zone, - color=zone_hsbk, - duration=duration, - apply=apply, - ) - await ack(set_zone) + try: + await self.coordinator.async_set_color_zones( + zone, zone, zone_hsbk, duration, apply + ) + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting color zones for {self.name}" + ) from ex - async def async_update(self): - """Update strip status.""" - if self.available and not self.lock.locked(): - await super().async_update() - await self.update_color_zones() + # set_color_zones does not update the + # state of the bulb, so we need to do that + await self.get_color() - async def update_color_zones(self): - """Get updated color information for each zone.""" - zone = 0 - top = 1 - while self.available and zone < top: - # Each get_color_zones can update 8 zones at once - resp = await AwaitAioLIFX().wait( - partial(self.bulb.get_color_zones, start_index=zone) - ) - if resp: - zone += 8 - top = resp.count - - # We only await multizone responses so don't ask for just one - if zone == top - 1: - zone -= 1 + async def update_color_zones( + self, + ) -> None: + """Send a get color zones message to the bulb.""" + try: + await self.coordinator.async_update_color_zones() + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting updating color zones for {self.name}" + ) from ex diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py new file mode 100644 index 00000000000..ee5428e36a8 --- /dev/null +++ b/homeassistant/components/lifx/manager.py @@ -0,0 +1,216 @@ +"""Support for LIFX lights.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +from typing import Any + +import aiolifx_effects +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_GROUP, + VALID_BRIGHTNESS, + VALID_BRIGHTNESS_PCT, + preprocess_turn_on_alternatives, +) +from homeassistant.const import ATTR_MODE +from homeassistant.core import HomeAssistant, ServiceCall, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_extract_referenced_entity_ids + +from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN +from .util import convert_8_to_16, find_hsbk + +SCAN_INTERVAL = timedelta(seconds=10) + + +SERVICE_EFFECT_PULSE = "effect_pulse" +SERVICE_EFFECT_COLORLOOP = "effect_colorloop" +SERVICE_EFFECT_STOP = "effect_stop" + +ATTR_POWER_ON = "power_on" +ATTR_PERIOD = "period" +ATTR_CYCLES = "cycles" +ATTR_SPREAD = "spread" +ATTR_CHANGE = "change" + +PULSE_MODE_BLINK = "blink" +PULSE_MODE_BREATHE = "breathe" +PULSE_MODE_PING = "ping" +PULSE_MODE_STROBE = "strobe" +PULSE_MODE_SOLID = "solid" + +PULSE_MODES = [ + PULSE_MODE_BLINK, + PULSE_MODE_BREATHE, + PULSE_MODE_PING, + PULSE_MODE_STROBE, + PULSE_MODE_SOLID, +] + +LIFX_EFFECT_SCHEMA = { + vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, +} + +LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) + ), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) + ), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + ), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), + ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), + ATTR_MODE: vol.In(PULSE_MODES), + } +) + +LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), + ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_TRANSITION: cv.positive_float, + } +) + +LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({}) + +SERVICES = ( + SERVICE_EFFECT_STOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_COLORLOOP, +) + + +class LIFXManager: + """Representation of all known LIFX entities.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the manager.""" + self.hass = hass + self.effects_conductor = aiolifx_effects.Conductor(hass.loop) + self.entry_id_to_entity_id: dict[str, str] = {} + + @callback + def async_unload(self) -> None: + """Release resources.""" + for service in SERVICES: + self.hass.services.async_remove(DOMAIN, service) + + @callback + def async_register_entity( + self, entity_id: str, entry_id: str + ) -> Callable[[], None]: + """Register an entity to the config entry id.""" + self.entry_id_to_entity_id[entry_id] = entity_id + + @callback + def unregister_entity() -> None: + """Unregister entity when it is being destroyed.""" + self.entry_id_to_entity_id.pop(entry_id) + + return unregister_entity + + @callback + def async_setup(self) -> None: + """Register the LIFX effects as hass service calls.""" + + async def service_handler(service: ServiceCall) -> None: + """Apply a service, i.e. start an effect.""" + referenced = async_extract_referenced_entity_ids(self.hass, service) + all_referenced = referenced.referenced | referenced.indirectly_referenced + if all_referenced: + await self.start_effect(all_referenced, service.service, **service.data) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_PULSE, + service_handler, + schema=LIFX_EFFECT_PULSE_SCHEMA, + ) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_COLORLOOP, + service_handler, + schema=LIFX_EFFECT_COLORLOOP_SCHEMA, + ) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_STOP, + service_handler, + schema=LIFX_EFFECT_STOP_SCHEMA, + ) + + async def start_effect( + self, entity_ids: set[str], service: str, **kwargs: Any + ) -> None: + """Start a light effect on entities.""" + bulbs = [ + coordinator.device + for entry_id, coordinator in self.hass.data[DOMAIN].items() + if entry_id != DATA_LIFX_MANAGER + and self.entry_id_to_entity_id[entry_id] in entity_ids + ] + _LOGGER.debug("Starting effect %s on %s", service, bulbs) + + if service == SERVICE_EFFECT_PULSE: + effect = aiolifx_effects.EffectPulse( + power_on=kwargs.get(ATTR_POWER_ON), + period=kwargs.get(ATTR_PERIOD), + cycles=kwargs.get(ATTR_CYCLES), + mode=kwargs.get(ATTR_MODE), + hsbk=find_hsbk(self.hass, **kwargs), + ) + await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_COLORLOOP: + preprocess_turn_on_alternatives(self.hass, kwargs) # type: ignore[no-untyped-call] + + brightness = None + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + effect = aiolifx_effects.EffectColorloop( + power_on=kwargs.get(ATTR_POWER_ON), + period=kwargs.get(ATTR_PERIOD), + change=kwargs.get(ATTR_CHANGE), + spread=kwargs.get(ATTR_SPREAD), + transition=kwargs.get(ATTR_TRANSITION), + brightness=brightness, + ) + await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_STOP: + await self.effects_conductor.stop(bulbs) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 06e7b292ac6..ebc4d73ce5d 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,12 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.8.1", "aiolifx_effects==0.2.2"], + "requirements": [ + "aiolifx==0.8.1", + "aiolifx_effects==0.2.2", + "aiolifx-connection==1.0.0" + ], + "quality_scale": "platinum", "dependencies": ["network"], "homekit": { "models": [ @@ -29,7 +34,8 @@ "LIFX Z" ] }, - "codeowners": ["@Djelibeybi"], + "dhcp": [{ "macaddress": "D073D5*" }, { "registered_devices": true }], + "codeowners": ["@bdraco", "@Djelibeybi"], "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"] } diff --git a/homeassistant/components/lifx/migration.py b/homeassistant/components/lifx/migration.py new file mode 100644 index 00000000000..359480a4507 --- /dev/null +++ b/homeassistant/components/lifx/migration.py @@ -0,0 +1,76 @@ +"""Migrate lifx devices to their own config entry.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .const import _LOGGER, DOMAIN +from .discovery import async_init_discovery_flow + + +@callback +def async_migrate_legacy_entries( + hass: HomeAssistant, + discovered_hosts_by_serial: dict[str, str], + existing_serials: set[str], + legacy_entry: ConfigEntry, +) -> int: + """Migrate the legacy config entries to have an entry per device.""" + _LOGGER.debug( + "Migrating legacy entries: discovered_hosts_by_serial=%s, existing_serials=%s", + discovered_hosts_by_serial, + existing_serials, + ) + + device_registry = dr.async_get(hass) + for dev_entry in dr.async_entries_for_config_entry( + device_registry, legacy_entry.entry_id + ): + for domain, serial in dev_entry.identifiers: + if ( + domain == DOMAIN + and serial not in existing_serials + and (host := discovered_hosts_by_serial.get(serial)) + ): + async_init_discovery_flow(hass, host, serial) + + remaining_devices = dr.async_entries_for_config_entry( + dr.async_get(hass), legacy_entry.entry_id + ) + _LOGGER.debug("The following devices remain: %s", remaining_devices) + return len(remaining_devices) + + +@callback +def async_migrate_entities_devices( + hass: HomeAssistant, legacy_entry_id: str, new_entry: ConfigEntry +) -> None: + """Move entities and devices to the new config entry.""" + migrated_devices = [] + device_registry = dr.async_get(hass) + for dev_entry in dr.async_entries_for_config_entry( + device_registry, legacy_entry_id + ): + for domain, value in dev_entry.identifiers: + if domain == DOMAIN and value == new_entry.unique_id: + _LOGGER.debug( + "Migrating device with %s to %s", + dev_entry.identifiers, + new_entry.unique_id, + ) + migrated_devices.append(dev_entry.id) + device_registry.async_update_device( + dev_entry.id, + add_config_entry_id=new_entry.entry_id, + remove_config_entry_id=legacy_entry_id, + ) + + entity_registry = er.async_get(hass) + for reg_entity in er.async_entries_for_config_entry( + entity_registry, legacy_entry_id + ): + if reg_entity.device_id in migrated_devices: + entity_registry.async_update_entity( + reg_entity.entity_id, config_entry_id=new_entry.entry_id + ) diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json index ebb8b39a8bc..b83ae9c1609 100644 --- a/homeassistant/components/lifx/strings.json +++ b/homeassistant/components/lifx/strings.json @@ -1,12 +1,28 @@ { "config": { + "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Do you want to set up LIFX?" + "user": { + "description": "If you leave the host empty, discovery will be used to find devices.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "pick_device": { + "data": { + "device": "Device" + } + }, + "discovery_confirm": { + "description": "Do you want to setup {label} ({host}) {serial}?" } }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } } diff --git a/homeassistant/components/lifx/translations/ca.json b/homeassistant/components/lifx/translations/ca.json index 28c25cde70a..8d0efc4de8a 100644 --- a/homeassistant/components/lifx/translations/ca.json +++ b/homeassistant/components/lifx/translations/ca.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "no_devices_found": "No s'han trobat dispositius a la xarxa", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Vols configurar LIFX?" + }, + "discovery_confirm": { + "description": "Vols configurar {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositiu" + } + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Si deixes l'amfitri\u00f3 buit, s'utilitzar\u00e0 el descobriment per cercar dispositius." } } } diff --git a/homeassistant/components/lifx/translations/de.json b/homeassistant/components/lifx/translations/de.json index 0c619ea4062..82e37b39c8b 100644 --- a/homeassistant/components/lifx/translations/de.json +++ b/homeassistant/components/lifx/translations/de.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "M\u00f6chtest du LIFX einrichten?" + }, + "discovery_confirm": { + "description": "M\u00f6chtest du {label} ({host}) {serial} einrichten?" + }, + "pick_device": { + "data": { + "device": "Ger\u00e4t" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Wenn du den Host leer l\u00e4sst, wird die Erkennung verwendet, um Ger\u00e4te zu finden." } } } diff --git a/homeassistant/components/lifx/translations/el.json b/homeassistant/components/lifx/translations/el.json index 5a1d7707f55..4ebea49190d 100644 --- a/homeassistant/components/lifx/translations/el.json +++ b/homeassistant/components/lifx/translations/el.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf LIFX;" + }, + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {label} ({host}) {serial};" + }, + "pick_device": { + "data": { + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0391\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b5\u03bd\u03cc, \u03b7 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." } } } diff --git a/homeassistant/components/lifx/translations/en.json b/homeassistant/components/lifx/translations/en.json index 154101995ac..1f7cf981f5d 100644 --- a/homeassistant/components/lifx/translations/en.json +++ b/homeassistant/components/lifx/translations/en.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, + "error": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Do you want to set up LIFX?" + }, + "discovery_confirm": { + "description": "Do you want to setup {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Device" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "If you leave the host empty, discovery will be used to find devices." } } } diff --git a/homeassistant/components/lifx/translations/et.json b/homeassistant/components/lifx/translations/et.json index ba833f79f8b..6d06cbb17ba 100644 --- a/homeassistant/components/lifx/translations/et.json +++ b/homeassistant/components/lifx/translations/et.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", "no_devices_found": "V\u00f5rgust ei leitud seadmeid", "single_instance_allowed": "Juba seadistatud, lubatud on ainult \u00fcks sidumine." }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Kas soovid seadistada LIFX-i?" + }, + "discovery_confirm": { + "description": "Kas seadistada {label} ( {host} ) {serial} ?" + }, + "pick_device": { + "data": { + "device": "Seade" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Kui j\u00e4tad hosti t\u00fchjaks kasutatakse seadmete leidmiseks avastamist." } } } diff --git a/homeassistant/components/lifx/translations/fr.json b/homeassistant/components/lifx/translations/fr.json index f8cc0a9dddd..c3f0561b085 100644 --- a/homeassistant/components/lifx/translations/fr.json +++ b/homeassistant/components/lifx/translations/fr.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Voulez-vous configurer LIFX?" + }, + "discovery_confirm": { + "description": "Voulez-vous configurer {label} ({host}) {serial}\u00a0?" + }, + "pick_device": { + "data": { + "device": "Appareil" + } + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } } diff --git a/homeassistant/components/lifx/translations/hu.json b/homeassistant/components/lifx/translations/hu.json index 3d728f21d07..588d5932e10 100644 --- a/homeassistant/components/lifx/translations/hu.json +++ b/homeassistant/components/lifx/translations/hu.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: LIFX?" + }, + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Eszk\u00f6z" + } + }, + "user": { + "data": { + "host": "C\u00edm" + }, + "description": "Ha \u00fcresen hagyja a c\u00edm mez\u0151t, a rendszer megpr\u00f3b\u00e1lja az eszk\u00f6z\u00f6ket megkeresni." } } } diff --git a/homeassistant/components/lifx/translations/id.json b/homeassistant/components/lifx/translations/id.json index 03b2b387c6f..8781581bb0f 100644 --- a/homeassistant/components/lifx/translations/id.json +++ b/homeassistant/components/lifx/translations/id.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Ingin menyiapkan LIFX?" + }, + "discovery_confirm": { + "description": "Ingin menyiapkan {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Perangkat" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Jika host dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat." } } } diff --git a/homeassistant/components/lifx/translations/it.json b/homeassistant/components/lifx/translations/it.json index be167ec9994..9e8c090ad0d 100644 --- a/homeassistant/components/lifx/translations/it.json +++ b/homeassistant/components/lifx/translations/it.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Vuoi configurare LIFX?" + }, + "discovery_confirm": { + "description": "Vuoi configurare {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Se lasci l'host vuoto, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi." } } } diff --git a/homeassistant/components/lifx/translations/ja.json b/homeassistant/components/lifx/translations/ja.json index 6cfa33a7ace..c3b144222a4 100644 --- a/homeassistant/components/lifx/translations/ja.json +++ b/homeassistant/components/lifx/translations/ja.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "LIFX\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" + }, + "discovery_confirm": { + "description": "{label} ({host}) {serial} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?" + }, + "pick_device": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9" + } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u767d\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/lifx/translations/pl.json b/homeassistant/components/lifx/translations/pl.json index a8ee3fa57ac..817867d7c62 100644 --- a/homeassistant/components/lifx/translations/pl.json +++ b/homeassistant/components/lifx/translations/pl.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Urz\u0105dzenie" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Je\u015bli nie podasz IP lub nazwy hosta, zostanie u\u017cyte wykrywanie do odnalezienia urz\u0105dze\u0144." } } } diff --git a/homeassistant/components/lifx/translations/pt-BR.json b/homeassistant/components/lifx/translations/pt-BR.json index f67284d8b5d..ee340af857e 100644 --- a/homeassistant/components/lifx/translations/pt-BR.json +++ b/homeassistant/components/lifx/translations/pt-BR.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "flow_title": "{label} ( {host} ) {serial}", "step": { "confirm": { "description": "Voc\u00ea quer configurar o LIFX?" + }, + "discovery_confirm": { + "description": "Deseja configurar {label} ( {host} ) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para localizar dispositivos." } } } diff --git a/homeassistant/components/lifx/translations/pt.json b/homeassistant/components/lifx/translations/pt.json index 56064a70e0d..594ac7dacc4 100644 --- a/homeassistant/components/lifx/translations/pt.json +++ b/homeassistant/components/lifx/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede.", - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do LIFX \u00e9 permitida." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/translations/ru.json b/homeassistant/components/lifx/translations/ru.json index 0d50dec498b..9e9a9460e19 100644 --- a/homeassistant/components/lifx/translations/ru.json +++ b/homeassistant/components/lifx/translations/ru.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c LIFX?" + }, + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0415\u0441\u043b\u0438 \u043d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." } } } diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json index ca4cfa92020..0f212e225be 100644 --- a/homeassistant/components/lifx/translations/tr.json +++ b/homeassistant/components/lifx/translations/tr.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "LIFX'i kurmak istiyor musunuz?" + }, + "discovery_confirm": { + "description": "{label} ( {host} ) {serial} kurmak istiyor musunuz?" + }, + "pick_device": { + "data": { + "device": "Cihaz" + } + }, + "user": { + "data": { + "host": "Sunucu" + }, + "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } } } diff --git a/homeassistant/components/lifx/translations/zh-Hant.json b/homeassistant/components/lifx/translations/zh-Hant.json index 911eaa570d1..e8ff08be901 100644 --- a/homeassistant/components/lifx/translations/zh-Hant.json +++ b/homeassistant/components/lifx/translations/zh-Hant.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a LIFX\uff1f" + }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {label} ({host}) {serial}\uff1f" + }, + "pick_device": { + "data": { + "device": "\u88dd\u7f6e" + } + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py new file mode 100644 index 00000000000..1de8bdae76a --- /dev/null +++ b/homeassistant/components/lifx/util.py @@ -0,0 +1,161 @@ +"""Support for LIFX.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from typing import Any + +from aiolifx import products +from aiolifx.aiolifx import Light +from aiolifx.message import Message +import async_timeout +from awesomeversion import AwesomeVersion + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, + preprocess_turn_on_alternatives, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +import homeassistant.util.color as color_util + +from .const import _LOGGER, DOMAIN, OVERALL_TIMEOUT + +FIX_MAC_FW = AwesomeVersion("3.70") + + +@callback +def async_entry_is_legacy(entry: ConfigEntry) -> bool: + """Check if a config entry is the legacy shared one.""" + return entry.unique_id is None or entry.unique_id == DOMAIN + + +@callback +def async_get_legacy_entry(hass: HomeAssistant) -> ConfigEntry | None: + """Get the legacy config entry.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if async_entry_is_legacy(entry): + return entry + return None + + +def convert_8_to_16(value: int) -> int: + """Scale an 8 bit level into 16 bits.""" + return (value << 8) | value + + +def convert_16_to_8(value: int) -> int: + """Scale a 16 bit level into 8 bits.""" + return value >> 8 + + +def lifx_features(bulb: Light) -> dict[str, Any]: + """Return a feature map for this bulb, or a default map if unknown.""" + features: dict[str, Any] = ( + products.features_map.get(bulb.product) or products.features_map[1] + ) + return features + + +def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | None: + """Find the desired color from a number of possible inputs. + + Hue, Saturation, Brightness, Kelvin + """ + hue, saturation, brightness, kelvin = [None] * 4 + + preprocess_turn_on_alternatives(hass, kwargs) # type: ignore[no-untyped-call] + + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] + elif ATTR_RGB_COLOR in kwargs: + hue, saturation = color_util.color_RGB_to_hs(*kwargs[ATTR_RGB_COLOR]) + elif ATTR_XY_COLOR in kwargs: + hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) + + if hue is not None: + assert saturation is not None + hue = int(hue / 360 * 65535) + saturation = int(saturation / 100 * 65535) + kelvin = 3500 + + if ATTR_COLOR_TEMP in kwargs: + kelvin = int( + color_util.color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + ) + saturation = 0 + + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + hsbk = [hue, saturation, brightness, kelvin] + return None if hsbk == [None] * 4 else hsbk + + +def merge_hsbk( + base: list[float | int | None], change: list[float | int | None] +) -> list[float | int | None]: + """Copy change on top of base, except when None. + + Hue, Saturation, Brightness, Kelvin + """ + return [b if c is None else c for b, c in zip(base, change)] + + +def _get_mac_offset(mac_addr: str, offset: int) -> str: + octets = [int(octet, 16) for octet in mac_addr.split(":")] + octets[5] = (octets[5] + offset) % 256 + return ":".join(f"{octet:02x}" for octet in octets) + + +def _off_by_one_mac(firmware: str) -> bool: + """Check if the firmware version has the off by one mac.""" + return bool(firmware and AwesomeVersion(firmware) >= FIX_MAC_FW) + + +def get_real_mac_addr(mac_addr: str, firmware: str) -> str: + """Increment the last byte of the mac address by one for FW>3.70.""" + return _get_mac_offset(mac_addr, 1) if _off_by_one_mac(firmware) else mac_addr + + +def formatted_serial(serial_number: str) -> str: + """Format the serial number to match the HA device registry.""" + return dr.format_mac(serial_number) + + +def mac_matches_serial_number(mac_addr: str, serial_number: str) -> bool: + """Check if a mac address matches the serial number.""" + formatted_mac = dr.format_mac(mac_addr) + return bool( + formatted_serial(serial_number) == formatted_mac + or _get_mac_offset(serial_number, 1) == formatted_mac + ) + + +async def async_execute_lifx(method: Callable) -> Message: + """Execute a lifx coroutine and wait for a response.""" + future: asyncio.Future[Message] = asyncio.Future() + + def _callback(bulb: Light, message: Message) -> None: + if not future.done(): + # The future will get canceled out from under + # us by async_timeout when we hit the OVERALL_TIMEOUT + future.set_result(message) + + _LOGGER.debug("Sending LIFX command: %s", method) + + method(callb=_callback) + result = None + + async with async_timeout.timeout(OVERALL_TIMEOUT): + result = await future + + if result is None: + raise asyncio.TimeoutError("No response from LIFX bulb") + return result diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index d854e922ebd..5131ee52e67 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = system - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/litejet/translations/ja.json b/homeassistant/components/litejet/translations/ja.json index c26dc073113..8a7f367f66a 100644 --- a/homeassistant/components/litejet/translations/ja.json +++ b/homeassistant/components/litejet/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "open_failed": "\u6307\u5b9a\u3055\u308c\u305f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002" diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 90c432010d7..f3b150f2c1d 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,4 +1,5 @@ """The Litter-Robot integration.""" +from __future__ import annotations from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException @@ -31,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from ex if hub.account.robots: - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index c49d18c5257..fbe32fa9749 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -1,11 +1,16 @@ """Config flow for Litter-Robot integration.""" +from __future__ import annotations + +from collections.abc import Mapping import logging +from typing import Any from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN from .hub import LitterRobotHub @@ -22,7 +27,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 51b88bb4f79..501b71fbd06 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,29 +1,35 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations +from collections.abc import Callable, Coroutine from datetime import time import logging -from types import MethodType from typing import Any from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException +from typing_extensions import ParamSpec from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) import homeassistant.util.dt as dt_util from .const import DOMAIN from .hub import LitterRobotHub +_P = ParamSpec("_P") + _LOGGER = logging.getLogger(__name__) REFRESH_WAIT_TIME_SECONDS = 8 -class LitterRobotEntity(CoordinatorEntity): +class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): """Generic Litter-Robot entity representing common data and methods.""" def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: @@ -63,7 +69,10 @@ class LitterRobotControlEntity(LitterRobotEntity): self._refresh_callback: CALLBACK_TYPE | None = None async def perform_action_and_refresh( - self, action: MethodType, *args: Any, **kwargs: Any + self, + action: Callable[_P, Coroutine[Any, Any, bool]], + *args: _P.args, + **kwargs: _P.kwargs, ) -> bool: """Perform an action and initiates a refresh of the robot data after a few seconds.""" success = False @@ -82,7 +91,7 @@ class LitterRobotControlEntity(LitterRobotEntity): ) return success - async def async_call_later_callback(self, *_) -> None: + async def async_call_later_callback(self, *_: Any) -> None: """Perform refresh request on callback.""" self._refresh_callback = None await self.coordinator.async_request_refresh() @@ -92,7 +101,7 @@ class LitterRobotControlEntity(LitterRobotEntity): self.async_cancel_refresh_callback() @callback - def async_cancel_refresh_callback(self): + def async_cancel_refresh_callback(self) -> None: """Clear the refresh callback if it has not already fired.""" if self._refresh_callback is not None: self._refresh_callback() @@ -126,10 +135,10 @@ class LitterRobotConfigEntity(LitterRobotControlEntity): def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) - self._assumed_state: Any = None + self._assumed_state: bool | None = None async def perform_action_and_assume_state( - self, action: MethodType, assumed_state: Any + self, action: Callable[[bool], Coroutine[Any, Any, bool]], assumed_state: bool ) -> None: """Perform an action and assume the state passed in if call is successful.""" if await self.perform_action_and_refresh(action, assumed_state): diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 43d60e534ea..bde4c780482 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any from pylitterbot import Account from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException @@ -24,7 +25,7 @@ class LitterRobotHub: account: Account - def __init__(self, hass: HomeAssistant, data: Mapping) -> None: + def __init__(self, hass: HomeAssistant, data: Mapping[str, Any]) -> None: """Initialize the Litter-Robot hub.""" self._data = data diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 01deaa302cf..b6dd2a976c3 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -from typing import Any +from typing import Any, Union, cast from pylitterbot.robot import Robot @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, - StateType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE @@ -61,12 +60,12 @@ class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): self.entity_description = description @property - def native_value(self) -> StateType | datetime: + def native_value(self) -> float | datetime | str | None: """Return the state.""" if self.entity_description.should_report(self.robot): if isinstance(val := getattr(self.robot, self.entity_description.key), str): return val.lower() - return val + return cast(Union[float, datetime, None], val) return None @property @@ -88,13 +87,13 @@ ROBOT_SENSORS = [ name="Sleep Mode Start Time", key="sleep_mode_start_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, + should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] ), LitterRobotSensorEntityDescription( name="Sleep Mode End Time", key="sleep_mode_end_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, + should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] ), LitterRobotSensorEntityDescription( name="Last Seen", diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 4d302a0d4ae..5374add1e34 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -17,11 +17,11 @@ class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): """Litter-Robot Night Light Mode Switch.""" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.night_light_mode_enabled + return self.robot.night_light_mode_enabled # type: ignore[no-any-return] @property def icon(self) -> str: @@ -41,11 +41,11 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): """Litter-Robot Panel Lockout Switch.""" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.panel_lock_enabled + return self.robot.panel_lock_enabled # type: ignore[no-any-return] @property def icon(self) -> str: @@ -61,7 +61,9 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): await self.perform_action_and_assume_state(self.robot.set_panel_lockout, False) -ROBOT_SWITCHES: list[tuple[type[LitterRobotConfigEntity], str]] = [ +ROBOT_SWITCHES: list[ + tuple[type[LitterRobotNightLightModeSwitch | LitterRobotPanelLockoutSwitch], str] +] = [ (LitterRobotNightLightModeSwitch, "Night Light Mode"), (LitterRobotPanelLockoutSwitch, "Panel Lockout"), ] @@ -75,7 +77,7 @@ async def async_setup_entry( """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities = [] + entities: list[SwitchEntity] = [] for robot in hub.account.robots: for switch_class, switch_type in ROBOT_SWITCHES: entities.append(switch_class(robot=robot, entity_type=switch_type, hub=hub)) diff --git a/homeassistant/components/litterrobot/translations/pt.json b/homeassistant/components/litterrobot/translations/pt.json index 7953cf5625c..c2bf0536ccf 100644 --- a/homeassistant/components/litterrobot/translations/pt.json +++ b/homeassistant/components/litterrobot/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Conta j\u00e1 configurada" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index cc122187f47..b5ed762ef5d 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -10,7 +10,7 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up local_ip from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/local_ip/translations/ja.json b/homeassistant/components/local_ip/translations/ja.json index f5d2efd6613..6ba538c36ce 100644 --- a/homeassistant/components/local_ip/translations/ja.json +++ b/homeassistant/components/local_ip/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index fa4d510d648..5a796b976ff 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -121,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "Locative", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/locative/translations/ja.json b/homeassistant/components/locative/translations/ja.json index a4f03bde29f..ee9e6f5f1a1 100644 --- a/homeassistant/components/locative/translations/ja.json +++ b/homeassistant/components/locative/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index fff086b6c4e..cabf6342fac 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -190,7 +190,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_LOGI] = logi_circle - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def service_handler(service: ServiceCall) -> None: """Dispatch service calls to target entities.""" diff --git a/homeassistant/components/logi_circle/translations/hu.json b/homeassistant/components/logi_circle/translations/hu.json index 947f11e4907..1d24638705e 100644 --- a/homeassistant/components/logi_circle/translations/hu.json +++ b/homeassistant/components/logi_circle/translations/hu.json @@ -13,7 +13,7 @@ }, "step": { "auth": { - "description": "K\u00e9rj\u00fck, k\u00f6vesse az al\u00e1bbi linket, \u00e9s ** Fogadja el ** a LogiCircle -fi\u00f3kj\u00e1hoz val\u00f3 hozz\u00e1f\u00e9r\u00e9st, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot. \n\n [Link]({authorization_url})", + "description": "K\u00e9rem, k\u00f6vesse az al\u00e1bbi linket, \u00e9s **Fogadja el** a LogiCircle -fi\u00f3kj\u00e1hoz val\u00f3 hozz\u00e1f\u00e9r\u00e9st, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot. \n\n[Link]({authorization_url})", "title": "Hiteles\u00edt\u00e9s a LogiCircle seg\u00edts\u00e9g\u00e9vel" }, "user": { diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index a4cc66a8447..96ff9bc5056 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -2,17 +2,28 @@ from __future__ import annotations from datetime import timedelta +import logging +import async_timeout from london_tube_status import TubeData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "london_underground" ATTRIBUTION = "Powered by TfL Open Data" @@ -55,24 +66,46 @@ async def async_setup_platform( session = async_get_clientsession(hass) data = TubeData(session) - await data.update() + coordinator = LondonTubeCoordinator(hass, data) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise PlatformNotReady sensors = [] for line in config[CONF_LINE]: - sensors.append(LondonTubeSensor(line, data)) + sensors.append(LondonTubeSensor(coordinator, line)) - async_add_entities(sensors, True) + async_add_entities(sensors) -class LondonTubeSensor(SensorEntity): +class LondonTubeCoordinator(DataUpdateCoordinator): + """London Underground sensor coordinator.""" + + def __init__(self, hass, data): + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + self._data = data + + async def _async_update_data(self): + async with async_timeout.timeout(10): + await self._data.update() + return self._data.data + + +class LondonTubeSensor(CoordinatorEntity[LondonTubeCoordinator], SensorEntity): """Sensor that reads the status of a line from Tube Data.""" - def __init__(self, name, data): + def __init__(self, coordinator, name): """Initialize the London Underground sensor.""" - self._data = data - self._description = None + super().__init__(coordinator) self._name = name - self._state = None self.attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} @property @@ -83,7 +116,7 @@ class LondonTubeSensor(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return self._state + return self.coordinator.data[self.name]["State"] @property def icon(self): @@ -93,11 +126,5 @@ class LondonTubeSensor(SensorEntity): @property def extra_state_attributes(self): """Return other details about the sensor state.""" - self.attrs["Description"] = self._description + self.attrs["Description"] = self.coordinator.data[self.name]["Description"] return self.attrs - - async def async_update(self): - """Update the sensor.""" - await self._data.update() - self._state = self._data.data[self.name]["State"] - self._description = self._data.data[self.name]["Description"] diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index 9b0a5b05f1f..5ab4078eb20 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -164,7 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_coordinators=device_coordinators, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/lookin/translations/pt.json b/homeassistant/components/lookin/translations/pt.json new file mode 100644 index 00000000000..03fd6fa7632 --- /dev/null +++ b/homeassistant/components/lookin/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 9b268dfdfcd..cf74a3a588c 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -86,11 +86,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config = await async_process_component_config(hass, conf, integration) + if config is None: + raise HomeAssistantError("Config validation failed") + resource_collection = await create_yaml_resource_col( hass, config[DOMAIN].get(CONF_RESOURCES) ) hass.data[DOMAIN]["resources"] = resource_collection + default_config: dashboard.LovelaceConfig if mode == MODE_YAML: default_config = dashboard.LovelaceYAML(hass, None, None) resource_collection = await create_yaml_resource_col(hass, yaml_resources) @@ -179,8 +183,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Process YAML dashboards for url_path, dashboard_conf in hass.data[DOMAIN]["yaml_dashboards"].items(): # For now always mode=yaml - config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) - hass.data[DOMAIN]["dashboards"][url_path] = config + lovelace_config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) + hass.data[DOMAIN]["dashboards"][url_path] = lovelace_config try: _register_panel(hass, url_path, MODE_YAML, dashboard_conf, False) diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 22297c54d6c..7e7c670baf5 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -7,7 +7,7 @@ import uuid import voluptuous as vol -from homeassistant.const import CONF_RESOURCES, CONF_TYPE +from homeassistant.const import CONF_ID, CONF_RESOURCES, CONF_TYPE from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, storage @@ -94,7 +94,7 @@ class ResourceStorageCollection(collection.StorageCollection): conf.pop(CONF_RESOURCES) for item in data: - item[collection.CONF_ID] = uuid.uuid4().hex + item[CONF_ID] = uuid.uuid4().hex data = {"items": data} diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index 45a042c1f2e..66ec7f22b09 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -37,16 +37,13 @@ def _handle_errors(func): connection.send_error(msg["id"], *error) return - if msg is not None: - await connection.send_big_result(msg["id"], result) - else: - connection.send_result(msg["id"], result) + connection.send_result(msg["id"], result) return send_with_error_handling -@websocket_api.async_response @websocket_api.websocket_command({"type": "lovelace/resources"}) +@websocket_api.async_response async def websocket_lovelace_resources(hass, connection, msg): """Send Lovelace UI resources over WebSocket configuration.""" resources = hass.data[DOMAIN]["resources"] @@ -58,7 +55,6 @@ async def websocket_lovelace_resources(hass, connection, msg): connection.send_result(msg["id"], resources.async_items()) -@websocket_api.async_response @websocket_api.websocket_command( { "type": "lovelace/config", @@ -66,6 +62,7 @@ async def websocket_lovelace_resources(hass, connection, msg): vol.Optional(CONF_URL_PATH): vol.Any(None, cv.string), } ) +@websocket_api.async_response @_handle_errors async def websocket_lovelace_config(hass, connection, msg, config): """Send Lovelace UI config over WebSocket configuration.""" @@ -73,7 +70,6 @@ async def websocket_lovelace_config(hass, connection, msg, config): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { "type": "lovelace/config/save", @@ -81,6 +77,7 @@ async def websocket_lovelace_config(hass, connection, msg, config): vol.Optional(CONF_URL_PATH): vol.Any(None, cv.string), } ) +@websocket_api.async_response @_handle_errors async def websocket_lovelace_save_config(hass, connection, msg, config): """Save Lovelace UI configuration.""" @@ -88,13 +85,13 @@ async def websocket_lovelace_save_config(hass, connection, msg, config): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { "type": "lovelace/config/delete", vol.Optional(CONF_URL_PATH): vol.Any(None, cv.string), } ) +@websocket_api.async_response @_handle_errors async def websocket_lovelace_delete_config(hass, connection, msg, config): """Delete Lovelace UI configuration.""" diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index d29a358fb61..d842fdc1a89 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 055820de3a0..5333d86a708 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -97,6 +97,7 @@ class SensorCommunitySensor(CoordinatorEntity, SensorEntity): """Implementation of a Sensor.Community sensor.""" _attr_attribution = "Data provided by Sensor.Community" + _attr_has_entity_name = True _attr_should_poll = False def __init__( diff --git a/homeassistant/components/luftdaten/translations/pt.json b/homeassistant/components/luftdaten/translations/pt.json index 709ed6af0a1..12e4c078d8c 100644 --- a/homeassistant/components/luftdaten/translations/pt.json +++ b/homeassistant/components/luftdaten/translations/pt.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Mostrar no mapa", - "station_id": "Luftdaten Sensor ID" + "station_id": "Sensor ID" } } } diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index b2b01aa3c44..1583d8b74eb 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -124,9 +124,7 @@ class LutronDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.async_add_executor_job( - self._lutron_device.subscribe, self._update_callback, None - ) + self._lutron_device.subscribe(self._update_callback, None) def _update_callback(self, _device, _context, _event, _params): """Run when invoked by pylutron when the device state changes.""" diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index c6ad8781478..d235f294b3a 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -143,8 +143,6 @@ async def async_setup_entry( ca_certs = hass.config.path(config_entry.data[CONF_CA_CERTS]) bridge = None - await _async_migrate_unique_ids(hass, config_entry) - try: bridge = Smartbridge.create_tls( hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs @@ -167,6 +165,7 @@ async def async_setup_entry( raise ConfigEntryNotReady(f"Cannot connect to {host}") _LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host) + await _async_migrate_unique_ids(hass, config_entry) devices = bridge.get_devices() bridge_device = devices[BRIDGE_DEVICE_ID] @@ -188,7 +187,7 @@ async def async_setup_entry( bridge, bridge_device, button_devices ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True @@ -251,6 +250,18 @@ def _area_and_name_from_name(device_name: str) -> tuple[str, str]: return UNASSIGNED_AREA, device_name +@callback +def async_get_lip_button(device_type: str, leap_button: int) -> int | None: + """Get the LIP button for a given LEAP button.""" + if ( + lip_buttons_name_to_num := DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device_type) + ) is None or ( + leap_button_num_to_name := LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get(device_type) + ) is None: + return None + return lip_buttons_name_to_num[leap_button_num_to_name[leap_button]] + + @callback def _async_subscribe_pico_remote_events( hass: HomeAssistant, @@ -272,21 +283,8 @@ def _async_subscribe_pico_remote_events( type_ = device["type"] area, name = _area_and_name_from_name(device["name"]) - button_number = device["button_number"] - # The original implementation used LIP instead of LEAP - # so we need to convert the button number to maintain compat - sub_type_to_lip_button = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP[type_] - leap_button_to_sub_type = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP[type_] - if (sub_type := leap_button_to_sub_type.get(button_number)) is None: - _LOGGER.error( - "Unknown LEAP button number %s is not in %s for %s (%s)", - button_number, - leap_button_to_sub_type, - name, - type_, - ) - return - lip_button_number = sub_type_to_lip_button[sub_type] + leap_button_number = device["button_number"] + lip_button_number = async_get_lip_button(type_, leap_button_number) hass_device = dev_reg.async_get_device({(DOMAIN, device["serial"])}) hass.bus.async_fire( @@ -295,7 +293,7 @@ def _async_subscribe_pico_remote_events( ATTR_SERIAL: device["serial"], ATTR_TYPE: type_, ATTR_BUTTON_NUMBER: lip_button_number, - ATTR_LEAP_BUTTON_NUMBER: button_number, + ATTR_LEAP_BUTTON_NUMBER: leap_button_number, ATTR_DEVICE_NAME: name, ATTR_DEVICE_ID: hass_device.id, ATTR_AREA_NAME: area, diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index ed809e0994a..27227619d45 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -27,7 +28,7 @@ from .const import ( ACTION_PRESS, ACTION_RELEASE, ATTR_ACTION, - ATTR_BUTTON_NUMBER, + ATTR_LEAP_BUTTON_NUMBER, ATTR_SERIAL, CONF_SUBTYPE, DOMAIN, @@ -35,6 +36,12 @@ from .const import ( ) from .models import LutronCasetaData + +def _reverse_dict(forward_dict: dict) -> dict: + """Reverse a dictionary.""" + return {v: k for k, v in forward_dict.items()} + + SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE] LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( @@ -52,9 +59,6 @@ PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP = { "on": 0, "off": 2, } -LEAP_TO_PICO_2_BUTTON_BUTTON_TYPES = { - v: k for k, v in PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP.items() -} PICO_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_2_BUTTON_BUTTON_TYPES_TO_LIP), @@ -74,9 +78,6 @@ PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { "raise": 3, "lower": 4, } -LEAP_TO_PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES = { - v: k for k, v in PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP.items() -} PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In( @@ -96,9 +97,6 @@ PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP = { "stop": 1, "off": 2, } -LEAP_TO_PICO_3_BUTTON_BUTTON_TYPES = { - v: k for k, v in PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP.items() -} PICO_3_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_3_BUTTON_BUTTON_TYPES_TO_LIP), @@ -119,9 +117,6 @@ PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { "raise": 3, "lower": 4, } -LEAP_TO_PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES = { - v: k for k, v in PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP.items() -} PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In( @@ -186,9 +181,6 @@ PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP = { "button_3": 3, "off": 4, } -LEAP_TO_PICO_4_BUTTON_SCENE_BUTTON_TYPES = { - v: k for k, v in PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP.items() -} PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LIP), @@ -208,9 +200,6 @@ PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP = { "group_2_button_1": 3, "group_2_button_2": 4, } -LEAP_TO_PICO_4_BUTTON_2_GROUP_BUTTON_TYPES = { - v: k for k, v in PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP.items() -} PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP), @@ -271,15 +260,58 @@ FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP = { "raise_4": 23, "lower_4": 24, } -LEAP_TO_FOUR_GROUP_REMOTE_BUTTON_TYPES = { - v: k for k, v in FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP.items() -} FOUR_GROUP_REMOTE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP), } ) + +SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, +} +SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In( + SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP + ), + } +) + + +SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, + "button_3": 3, + "raise": 19, + "lower": 18, +} +SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = ( + LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In( + SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP + ), + } + ) +) + +SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, + "button_3": 3, + "button_4": 4, +} +SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In( + SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP + ), + } +) + + DEVICE_TYPE_SCHEMA_MAP = { "Pico2Button": PICO_2_BUTTON_TRIGGER_SCHEMA, "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, @@ -290,6 +322,9 @@ DEVICE_TYPE_SCHEMA_MAP = { "Pico4ButtonZone": PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, "FourGroupRemote": FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, + "SunnataKeypad_2Button": SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA, + "SunnataKeypad_3ButtonRaiseLower": SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, + "SunnataKeypad_4Button": SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA, } DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { @@ -304,16 +339,23 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP, } +DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = { + "Pico2Button": PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP, + "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, + "Pico3Button": PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP, + "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, + "Pico4Button": PICO_4_BUTTON_BUTTON_TYPES_TO_LEAP, + "Pico4ButtonScene": PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP, + "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP, + "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP, + "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP, + "SunnataKeypad_2Button": SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP, + "SunnataKeypad_3ButtonRaiseLower": SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, + "SunnataKeypad_4Button": SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP, +} + LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP = { - "Pico2Button": LEAP_TO_PICO_2_BUTTON_BUTTON_TYPES, - "Pico2ButtonRaiseLower": LEAP_TO_PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES, - "Pico3Button": LEAP_TO_PICO_3_BUTTON_BUTTON_TYPES, - "Pico3ButtonRaiseLower": LEAP_TO_PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES, - "Pico4Button": LEAP_TO_PICO_4_BUTTON_BUTTON_TYPES, - "Pico4ButtonScene": LEAP_TO_PICO_4_BUTTON_SCENE_BUTTON_TYPES, - "Pico4ButtonZone": LEAP_TO_PICO_4_BUTTON_ZONE_BUTTON_TYPES, - "Pico4Button2Group": LEAP_TO_PICO_4_BUTTON_2_GROUP_BUTTON_TYPES, - "FourGroupRemote": LEAP_TO_FOUR_GROUP_REMOTE_BUTTON_TYPES, + k: _reverse_dict(v) for k, v in DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.items() } TRIGGER_SCHEMA = vol.Any( @@ -324,6 +366,9 @@ TRIGGER_SCHEMA = vol.Any( PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, + SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA, + SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, + SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA, ) @@ -354,7 +399,7 @@ async def async_get_triggers( if not (device := get_button_device_by_dr_id(hass, device_id)): raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device["type"], {}) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(device["type"], {}) for trigger in SUPPORTED_INPUTS_EVENTS_TYPES: for subtype in valid_buttons: @@ -385,20 +430,24 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Attach a trigger.""" device_registry = dr.async_get(hass) - device = device_registry.async_get(config[CONF_DEVICE_ID]) - assert device - assert device.model + if ( + not (device := device_registry.async_get(config[CONF_DEVICE_ID])) + or not device.model + ): + raise HomeAssistantError( + f"Cannot attach trigger {config} because device with id {config[CONF_DEVICE_ID]} is missing or invalid" + ) device_type = _device_model_to_type(device.model) _, serial = list(device.identifiers)[0] schema = DEVICE_TYPE_SCHEMA_MAP[device_type] - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP[device_type] + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP[device_type] config = schema(config) event_config = { event_trigger.CONF_PLATFORM: CONF_EVENT, event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, event_trigger.CONF_EVENT_DATA: { ATTR_SERIAL: serial, - ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], + ATTR_LEAP_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], ATTR_ACTION: config[CONF_TYPE], }, } diff --git a/homeassistant/components/lutron_caseta/translations/pt.json b/homeassistant/components/lutron_caseta/translations/pt.json index a04f550a71a..4ae57417d6f 100644 --- a/homeassistant/components/lutron_caseta/translations/pt.json +++ b/homeassistant/components/lutron_caseta/translations/pt.json @@ -6,6 +6,19 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + }, + "device_automation": { + "trigger_subtype": { + "on": "Ligado", + "raise_3": "Aumentar 3" } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 2eaee440ae7..d1048ac8e58 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -11,14 +11,10 @@ from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import async_timeout -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import ( @@ -42,20 +38,7 @@ from .api import ( ) from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) @@ -63,28 +46,17 @@ PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Honeywell Lyric component.""" - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - - _LOGGER.warning( - "Configuration of Honeywell Lyric integration in YAML is deprecated " - "and will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) + """Set up the Honeywell Lyric integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) return True @@ -143,12 +115,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=timedelta(seconds=300), ) - hass.data[DOMAIN][entry.entry_id] = coordinator - # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index b5c1fb05efa..8353ae15b3c 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import logging from time import localtime, strftime, time +from typing import Any from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation @@ -186,7 +187,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return self.device.indoorTemperature @property - def hvac_action(self) -> HVACAction: + def hvac_action(self) -> HVACAction | None: """Return the current hvac action.""" action = HVAC_ACTIONS.get(self.device.operationStatus.mode, None) if action == HVACAction.OFF and self.hvac_mode != HVACMode.OFF: @@ -265,7 +266,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return device.maxHeatSetpoint return device.maxCoolSetpoint - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if self.hvac_mode == HVACMode.OFF: return diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 12f91cfe206..de6808dd0de 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Honeywell Lyric.""" +from __future__ import annotations + from collections.abc import Mapping import logging from typing import Any @@ -25,13 +27,15 @@ class OAuth2FlowHandler( """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an oauth config entry or update existing entry for reauth.""" existing_entry = await self.async_set_unique_id(DOMAIN) if existing_entry: diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index c0d9168f46f..91b152cdf21 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Lyric", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "requirements": ["aiolyric==1.0.8"], "codeowners": ["@timmo001"], "quality_scale": "silver", diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 5b60935d667..d727b24eee4 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -47,9 +47,11 @@ class LyricSensorEntityDescription(SensorEntityDescription): value: Callable[[LyricDevice], StateType | datetime] = round -def get_datetime_from_future_time(time: str) -> datetime: +def get_datetime_from_future_time(time_str: str) -> datetime: """Get datetime from future time provided.""" - time = dt_util.parse_time(time) + time = dt_util.parse_time(time_str) + if time is None: + raise ValueError(f"Unable to parse time {time_str}") now = dt_util.utcnow() if time <= now.time(): now = now + timedelta(days=1) @@ -64,7 +66,7 @@ async def async_setup_entry( entities = [] - def get_setpoint_status(status: str, time: str) -> str: + def get_setpoint_status(status: str, time: str) -> str | None: if status == PRESET_HOLD_UNTIL: return f"Held until {time}" return LYRIC_SETPOINT_STATUS_NAMES.get(status, None) diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 3c9cd6043df..dd9a89f294d 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -17,5 +17,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "removed_yaml": { + "title": "The Honeywell Lyric YAML configuration has been removed", + "description": "Configuring Honeywell Lyric using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/lyric/translations/bg.json b/homeassistant/components/lyric/translations/bg.json new file mode 100644 index 00000000000..2f756377e31 --- /dev/null +++ b/homeassistant/components/lyric/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json index b067b299145..1ef7e65fbf4 100644 --- a/homeassistant/components/lyric/translations/de.json +++ b/homeassistant/components/lyric/translations/de.json @@ -17,5 +17,11 @@ "title": "Integration erneut authentifizieren" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Honeywell Lyric mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Honeywell Lyric YAML-Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/el.json b/homeassistant/components/lyric/translations/el.json index e9321950874..a16d2844dcb 100644 --- a/homeassistant/components/lyric/translations/el.json +++ b/homeassistant/components/lyric/translations/el.json @@ -17,5 +17,11 @@ "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } + }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Honeywell Lyric \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03b7\u03c2 Honeywell Lyric \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json index 17586f16109..3d1df448ba2 100644 --- a/homeassistant/components/lyric/translations/en.json +++ b/homeassistant/components/lyric/translations/en.json @@ -17,5 +17,11 @@ "title": "Reauthenticate Integration" } } + }, + "issues": { + "removed_yaml": { + "description": "Configuring Honeywell Lyric using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Honeywell Lyric YAML configuration has been removed" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json index 6fb3fb44275..e0e97ef3246 100644 --- a/homeassistant/components/lyric/translations/it.json +++ b/homeassistant/components/lyric/translations/it.json @@ -17,5 +17,11 @@ "title": "Autentica nuovamente l'integrazione" } } + }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Honeywell Lyric tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Honeywell Lyric \u00e8 stata rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index 98b2a11c5ff..2a5abdcb5b7 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -13,8 +13,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "(\u6b4c\u8a5e)Lyric\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "(\u6b4c\u8a5e)Lyric\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/lyric/translations/pl.json b/homeassistant/components/lyric/translations/pl.json index 09ae3ba273a..bddf67f62e8 100644 --- a/homeassistant/components/lyric/translations/pl.json +++ b/homeassistant/components/lyric/translations/pl.json @@ -17,5 +17,11 @@ "title": "Ponownie uwierzytelnij integracj\u0119" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Honeywell Lyric za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Honeywell Lyric zosta\u0142a usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pt-BR.json b/homeassistant/components/lyric/translations/pt-BR.json index 907a396d5e2..d70832bfbf6 100644 --- a/homeassistant/components/lyric/translations/pt-BR.json +++ b/homeassistant/components/lyric/translations/pt-BR.json @@ -17,5 +17,11 @@ "title": "Reautenticar Integra\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Honeywell Lyric usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Honeywell Lyric YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pt.json b/homeassistant/components/lyric/translations/pt.json new file mode 100644 index 00000000000..002029ae6f7 --- /dev/null +++ b/homeassistant/components/lyric/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json index 850507ec0b3..bb7fbc3aed6 100644 --- a/homeassistant/components/lyric/translations/zh-Hant.json +++ b/homeassistant/components/lyric/translations/zh-Hant.json @@ -17,5 +17,11 @@ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Honeywell Lyric \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Honeywell Lyric YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/ja.json b/homeassistant/components/mailgun/translations/ja.json index 34c6ced3f38..9c0f731f8fb 100644 --- a/homeassistant/components/mailgun/translations/ja.json +++ b/homeassistant/components/mailgun/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 47646a586ca..17df46a0849 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.util.json import load_json, save_json -from .const import DOMAIN, SERVICE_SEND_MESSAGE +from .const import DOMAIN, FORMAT_HTML, FORMAT_TEXT, SERVICE_SEND_MESSAGE _LOGGER = logging.getLogger(__name__) @@ -36,8 +36,12 @@ CONF_EXPRESSION = "expression" DEFAULT_CONTENT_TYPE = "application/octet-stream" +MESSAGE_FORMATS = [FORMAT_HTML, FORMAT_TEXT] +DEFAULT_MESSAGE_FORMAT = FORMAT_TEXT + EVENT_MATRIX_COMMAND = "matrix_command" +ATTR_FORMAT = "format" # optional message format ATTR_IMAGES = "images" # optional images COMMAND_SCHEMA = vol.All( @@ -74,7 +78,10 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema( { vol.Required(ATTR_MESSAGE): cv.string, - vol.Optional(ATTR_DATA): { + vol.Optional(ATTR_DATA, default={}): { + vol.Optional(ATTR_FORMAT, default=DEFAULT_MESSAGE_FORMAT): vol.In( + MESSAGE_FORMATS + ), vol.Optional(ATTR_IMAGES): vol.All(cv.ensure_list, [cv.string]), }, vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), @@ -377,7 +384,10 @@ class MatrixBot: try: room = self._join_or_get_room(target_room) if message is not None: - _LOGGER.debug(room.send_text(message)) + if data.get(ATTR_FORMAT) == FORMAT_HTML: + _LOGGER.debug(room.send_html(message)) + else: + _LOGGER.debug(room.send_text(message)) except MatrixRequestError as ex: _LOGGER.error( "Unable to deliver message to room '%s': %d, %s", @@ -385,7 +395,7 @@ class MatrixBot: ex.code, ex.content, ) - if data is not None: + if ATTR_IMAGES in data: for img in data.get(ATTR_IMAGES, []): self._send_image(img, target_rooms) diff --git a/homeassistant/components/matrix/const.py b/homeassistant/components/matrix/const.py index 6b082bde121..b7e0c22e2ac 100644 --- a/homeassistant/components/matrix/const.py +++ b/homeassistant/components/matrix/const.py @@ -2,3 +2,6 @@ DOMAIN = "matrix" SERVICE_SEND_MESSAGE = "send_message" + +FORMAT_HTML = "html" +FORMAT_TEXT = "text" diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index c58a27c3370..9b5171d1483 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -18,7 +18,7 @@ send_message: text: data: name: Data - description: Extended information of notification. Supports list of images. Optional. - example: "{'images': ['/tmp/test.jpg']}" + description: Extended information of notification. Supports list of images. Supports message format. Optional. + example: "{'images': ['/tmp/test.jpg'], 'format': 'text'}" selector: object: diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 85b9700a624..b62725e5b1b 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -192,7 +192,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() # Setup components - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Register services hass.services.async_register( @@ -222,6 +222,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class MazdaEntity(CoordinatorEntity): """Defines a base Mazda entity.""" + _attr_has_entity_name = True + def __init__(self, client, coordinator, index): """Initialize the Mazda entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/mazda/binary_sensor.py b/homeassistant/components/mazda/binary_sensor.py index cc60d6318c7..c2727654525 100644 --- a/homeassistant/components/mazda/binary_sensor.py +++ b/homeassistant/components/mazda/binary_sensor.py @@ -22,9 +22,6 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN class MazdaBinarySensorRequiredKeysMixin: """Mixin for required keys.""" - # Suffix to be appended to the vehicle name to obtain the binary sensor name - name_suffix: str - # Function to determine the value for this binary sensor, given the coordinator data value_fn: Callable[[dict[str, Any]], bool] @@ -49,49 +46,49 @@ def _plugged_in_supported(data): BINARY_SENSOR_ENTITIES = [ MazdaBinarySensorEntityDescription( key="driver_door", - name_suffix="Driver Door", + name="Driver door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"], ), MazdaBinarySensorEntityDescription( key="passenger_door", - name_suffix="Passenger Door", + name="Passenger door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"], ), MazdaBinarySensorEntityDescription( key="rear_left_door", - name_suffix="Rear Left Door", + name="Rear left door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"], ), MazdaBinarySensorEntityDescription( key="rear_right_door", - name_suffix="Rear Right Door", + name="Rear right door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"], ), MazdaBinarySensorEntityDescription( key="trunk", - name_suffix="Trunk", + name="Trunk", icon="mdi:car-back", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["trunkOpen"], ), MazdaBinarySensorEntityDescription( key="hood", - name_suffix="Hood", + name="Hood", icon="mdi:car", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["hoodOpen"], ), MazdaBinarySensorEntityDescription( key="ev_plugged_in", - name_suffix="Plugged In", + name="Plugged in", device_class=BinarySensorDeviceClass.PLUG, is_supported=_plugged_in_supported, value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"], @@ -126,7 +123,6 @@ class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity): super().__init__(client, coordinator, index) self.entity_description = description - self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" @property diff --git a/homeassistant/components/mazda/button.py b/homeassistant/components/mazda/button.py index e747cb33dc2..d9b21df5156 100644 --- a/homeassistant/components/mazda/button.py +++ b/homeassistant/components/mazda/button.py @@ -61,17 +61,7 @@ async def handle_refresh_vehicle_status( @dataclass -class MazdaButtonRequiredKeysMixin: - """Mixin for required keys.""" - - # Suffix to be appended to the vehicle name to obtain the button name - name_suffix: str - - -@dataclass -class MazdaButtonEntityDescription( - ButtonEntityDescription, MazdaButtonRequiredKeysMixin -): +class MazdaButtonEntityDescription(ButtonEntityDescription): """Describes a Mazda button entity.""" # Function to determine whether the vehicle supports this button, given the coordinator data @@ -85,27 +75,27 @@ class MazdaButtonEntityDescription( BUTTON_ENTITIES = [ MazdaButtonEntityDescription( key="start_engine", - name_suffix="Start Engine", + name="Start engine", icon="mdi:engine", ), MazdaButtonEntityDescription( key="stop_engine", - name_suffix="Stop Engine", + name="Stop engine", icon="mdi:engine-off", ), MazdaButtonEntityDescription( key="turn_on_hazard_lights", - name_suffix="Turn On Hazard Lights", + name="Turn on hazard lights", icon="mdi:hazard-lights", ), MazdaButtonEntityDescription( key="turn_off_hazard_lights", - name_suffix="Turn Off Hazard Lights", + name="Turn off hazard lights", icon="mdi:hazard-lights", ), MazdaButtonEntityDescription( key="refresh_vehicle_status", - name_suffix="Refresh Status", + name="Refresh status", icon="mdi:refresh", async_press=handle_refresh_vehicle_status, is_supported=lambda data: data["isElectric"], @@ -146,7 +136,6 @@ class MazdaButtonEntity(MazdaEntity, ButtonEntity): super().__init__(client, coordinator, index) self.entity_description = description - self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" async def async_press(self) -> None: diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py index 13266cd64d7..79a9ef90b9b 100644 --- a/homeassistant/components/mazda/device_tracker.py +++ b/homeassistant/components/mazda/device_tracker.py @@ -29,6 +29,7 @@ async def async_setup_entry( class MazdaDeviceTracker(MazdaEntity, TrackerEntity): """Class for the device tracker.""" + _attr_name = "Device tracker" _attr_icon = "mdi:car" _attr_force_update = False @@ -36,7 +37,6 @@ class MazdaDeviceTracker(MazdaEntity, TrackerEntity): """Initialize Mazda device tracker.""" super().__init__(client, coordinator, index) - self._attr_name = f"{self.vehicle_name} Device Tracker" self._attr_unique_id = self.vin @property diff --git a/homeassistant/components/mazda/lock.py b/homeassistant/components/mazda/lock.py index bcd409d2faf..1f42c5dce48 100644 --- a/homeassistant/components/mazda/lock.py +++ b/homeassistant/components/mazda/lock.py @@ -32,11 +32,12 @@ async def async_setup_entry( class MazdaLock(MazdaEntity, LockEntity): """Class for the lock.""" + _attr_name = "Lock" + def __init__(self, client, coordinator, index) -> None: """Initialize Mazda lock.""" super().__init__(client, coordinator, index) - self._attr_name = f"{self.vehicle_name} Lock" self._attr_unique_id = self.vin @property diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index acf5282689f..d521bc748e0 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.3.6"], + "requirements": ["pymazda==0.3.7"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 7e8b45e0ca1..c688ac62637 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -32,9 +32,6 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN class MazdaSensorRequiredKeysMixin: """Mixin for required keys.""" - # Suffix to be appended to the vehicle name to obtain the sensor name - name_suffix: str - # Function to determine the value for this sensor, given the coordinator data and the configured unit system value: Callable[[dict[str, Any], UnitSystem], StateType] @@ -159,7 +156,7 @@ def _ev_remaining_range_value(data, unit_system): SENSOR_ENTITIES = [ MazdaSensorEntityDescription( key="fuel_remaining_percentage", - name_suffix="Fuel Remaining Percentage", + name="Fuel remaining percentage", icon="mdi:gas-station", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -168,7 +165,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="fuel_distance_remaining", - name_suffix="Fuel Distance Remaining", + name="Fuel distance remaining", icon="mdi:gas-station", unit=_get_distance_unit, state_class=SensorStateClass.MEASUREMENT, @@ -177,7 +174,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="odometer", - name_suffix="Odometer", + name="Odometer", icon="mdi:speedometer", unit=_get_distance_unit, state_class=SensorStateClass.TOTAL_INCREASING, @@ -186,7 +183,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="front_left_tire_pressure", - name_suffix="Front Left Tire Pressure", + name="Front left tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -196,7 +193,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="front_right_tire_pressure", - name_suffix="Front Right Tire Pressure", + name="Front right tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -206,7 +203,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="rear_left_tire_pressure", - name_suffix="Rear Left Tire Pressure", + name="Rear left tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -216,7 +213,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="rear_right_tire_pressure", - name_suffix="Rear Right Tire Pressure", + name="Rear right tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -226,7 +223,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="ev_charge_level", - name_suffix="Charge Level", + name="Charge level", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -235,7 +232,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="ev_remaining_range", - name_suffix="Remaining Range", + name="Remaining range", icon="mdi:ev-station", unit=_get_distance_unit, state_class=SensorStateClass.MEASUREMENT, @@ -276,7 +273,6 @@ class MazdaSensorEntity(MazdaEntity, SensorEntity): super().__init__(client, coordinator, index) self.entity_description = description - self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" @property diff --git a/homeassistant/components/mazda/switch.py b/homeassistant/components/mazda/switch.py index 3ab3028425f..844585720d7 100644 --- a/homeassistant/components/mazda/switch.py +++ b/homeassistant/components/mazda/switch.py @@ -30,6 +30,7 @@ async def async_setup_entry( class MazdaChargingSwitch(MazdaEntity, SwitchEntity): """Class for the charging switch.""" + _attr_name = "Charging" _attr_icon = "mdi:ev-station" def __init__( @@ -41,7 +42,6 @@ class MazdaChargingSwitch(MazdaEntity, SwitchEntity): """Initialize Mazda charging switch.""" super().__init__(client, coordinator, index) - self._attr_name = f"{self.vehicle_name} Charging" self._attr_unique_id = self.vin @property diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json index 42afc763687..baa251ac301 100644 --- a/homeassistant/components/mazda/translations/hu.json +++ b/homeassistant/components/mazda/translations/hu.json @@ -17,7 +17,7 @@ "password": "Jelsz\u00f3", "region": "R\u00e9gi\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg azt az e-mail c\u00edmet \u00e9s jelsz\u00f3t, amelyet a MyMazda mobilalkalmaz\u00e1sba val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt." + "description": "K\u00e9rem, adja meg azt az e-mail c\u00edmet \u00e9s jelsz\u00f3t, amelyet a MyMazda mobilalkalmaz\u00e1sba val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt." } } } diff --git a/homeassistant/components/mazda/translations/pt.json b/homeassistant/components/mazda/translations/pt.json new file mode 100644 index 00000000000..70ac9ae7bf8 --- /dev/null +++ b/homeassistant/components/mazda/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py index 904d9e412c0..6db3093567d 100644 --- a/homeassistant/components/meater/__init__.py +++ b/homeassistant/components/meater/__init__.py @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "coordinator": coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 84ef3a2e2a9..a2753a42307 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -43,14 +43,18 @@ class MeaterSensorEntityDescription( def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert elapsed time to timestamp.""" - if not probe.cook: + if not probe.cook or not hasattr(probe.cook, "time_elapsed"): return None return dt_util.utcnow() - timedelta(seconds=probe.cook.time_elapsed) def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert remaining time to timestamp.""" - if not probe.cook or probe.cook.time_remaining < 0: + if ( + not probe.cook + or not hasattr(probe.cook, "time_remaining") + or probe.cook.time_remaining < 0 + ): return None return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining) @@ -99,7 +103,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.target_temperature if probe.cook else None, + value=lambda probe: probe.cook.target_temperature + if probe.cook and hasattr(probe.cook, "target_temperature") + else None, ), # Peak temperature MeaterSensorEntityDescription( @@ -109,7 +115,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.peak_temperature if probe.cook else None, + value=lambda probe: probe.cook.peak_temperature + if probe.cook and hasattr(probe.cook, "peak_temperature") + else None, ), # Remaining time in seconds. When unknown/calculating default is used. Default: -1 # Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time. diff --git a/homeassistant/components/meater/translations/hu.json b/homeassistant/components/meater/translations/hu.json index 964c240b4e3..98f54598ee4 100644 --- a/homeassistant/components/meater/translations/hu.json +++ b/homeassistant/components/meater/translations/hu.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "service_unavailable_error": "Az API jelenleg nem el\u00e9rhet\u0151, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "service_unavailable_error": "Az API jelenleg nem el\u00e9rhet\u0151, k\u00e9rem, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown_auth_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/meater/translations/pt.json b/homeassistant/components/meater/translations/pt.json new file mode 100644 index 00000000000..bc859189b94 --- /dev/null +++ b/homeassistant/components/meater/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "unknown_auth_error": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 14546a36ec8..9ca9613278d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -import base64 import collections from collections.abc import Callable from contextlib import suppress @@ -27,7 +26,6 @@ from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.websocket_api.const import ( - ERR_NOT_FOUND, ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) @@ -254,7 +252,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) - websocket_api.async_register_command(hass, websocket_handle_thumbnail) websocket_api.async_register_command(hass, websocket_browse_media) hass.http.register_view(MediaPlayerImageView(component)) @@ -1130,49 +1127,6 @@ class MediaPlayerImageView(HomeAssistantView): return web.Response(body=data, content_type=content_type, headers=headers) -@websocket_api.websocket_command( - { - vol.Required("type"): "media_player_thumbnail", - vol.Required("entity_id"): cv.entity_id, - } -) -@websocket_api.async_response -async def websocket_handle_thumbnail(hass, connection, msg): - """Handle get media player cover command. - - Async friendly. - """ - component = hass.data[DOMAIN] - - if (player := component.get_entity(msg["entity_id"])) is None: - connection.send_message( - websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") - ) - return - - _LOGGER.warning( - "The websocket command media_player_thumbnail is deprecated. Use /api/media_player_proxy instead" - ) - - data, content_type = await player.async_get_media_image() - - if data is None: - connection.send_message( - websocket_api.error_message( - msg["id"], "thumbnail_fetch_failed", "Failed to fetch thumbnail" - ) - ) - return - - await connection.send_big_result( - msg["id"], - { - "content_type": content_type, - "content": base64.b64encode(data).decode("utf-8"), - }, - ) - - @websocket_api.websocket_command( { vol.Required("type"): "media_player/browse_media", diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index b698b87aec6..5a513e4f3a0 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -257,11 +257,14 @@ join: group_members: name: Group members description: The players which will be synced with the target player. - example: - - "media_player.multiroom_player2" - - "media_player.multiroom_player3" + required: true + example: | + - media_player.multiroom_player2 + - media_player.multiroom_player3 selector: - object: + entity: + multiple: true + domain: media_player unjoin: description: diff --git a/homeassistant/components/media_player/translations/cs.json b/homeassistant/components/media_player/translations/cs.json index e20ec750f79..19e88635f8b 100644 --- a/homeassistant/components/media_player/translations/cs.json +++ b/homeassistant/components/media_player/translations/cs.json @@ -6,6 +6,10 @@ "is_on": "{entity_name} je zapnuto", "is_paused": "{entity_name} je pozastaven", "is_playing": "{entity_name} p\u0159ehr\u00e1v\u00e1" + }, + "trigger_type": { + "paused": "{entity_name} je pozastaveno", + "turned_on": "{entity_name} bylo zapnuto" } }, "state": { diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 6342c2bc3be..d8a044d23f6 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: conf = entry.data mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -142,7 +142,7 @@ class MelCloudDevice: ) -async def mel_devices_setup(hass, token) -> list[MelCloudDevice]: +async def mel_devices_setup(hass, token) -> dict[str, list[MelCloudDevice]]: """Query connected devices from MELCloud.""" session = async_get_clientsession(hass) try: @@ -156,7 +156,7 @@ async def mel_devices_setup(hass, token) -> list[MelCloudDevice]: except (asyncio.TimeoutError, ClientConnectionError) as ex: raise ConfigEntryNotReady() from ex - wrapped_devices = {} + wrapped_devices: dict[str, list[MelCloudDevice]] = {} for device_type, devices in all_devices.items(): wrapped_devices[device_type] = [MelCloudDevice(device) for device in devices] return wrapped_devices diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index b16221923de..a0ffe3a68bb 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -66,16 +66,19 @@ async def async_setup_entry( ) -> None: """Set up MelCloud device climate based on config_entry.""" mel_devices = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities: list[AtaDeviceClimate | AtwDeviceZoneClimate] = [ + AtaDeviceClimate(mel_device, mel_device.device) + for mel_device in mel_devices[DEVICE_TYPE_ATA] + ] + entities.extend( [ - AtaDeviceClimate(mel_device, mel_device.device) - for mel_device in mel_devices[DEVICE_TYPE_ATA] - ] - + [ AtwDeviceZoneClimate(mel_device, mel_device.device, zone) for mel_device in mel_devices[DEVICE_TYPE_ATW] for zone in mel_device.device.zones - ], + ] + ) + async_add_entities( + entities, True, ) @@ -157,14 +160,16 @@ class AtaDeviceClimate(MelCloudClimate): return attr @property - def hvac_mode(self) -> HVACMode: + def hvac_mode(self) -> HVACMode | None: """Return hvac operation ie. heat, cool mode.""" mode = self._device.operation_mode if not self._device.power or mode is None: return HVACMode.OFF return ATA_HVAC_MODE_LOOKUP.get(mode) - def _apply_set_hvac_mode(self, hvac_mode: str, set_dict: dict[str, Any]) -> None: + def _apply_set_hvac_mode( + self, hvac_mode: HVACMode, set_dict: dict[str, Any] + ) -> None: """Apply hvac mode changes to a dict used to call _device.set.""" if hvac_mode == HVACMode.OFF: set_dict["power"] = False @@ -180,7 +185,7 @@ class AtaDeviceClimate(MelCloudClimate): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - set_dict = {} + set_dict: dict[str, Any] = {} self._apply_set_hvac_mode(hvac_mode, set_dict) await self._device.set(set_dict) @@ -188,7 +193,9 @@ class AtaDeviceClimate(MelCloudClimate): def hvac_modes(self) -> list[HVACMode]: """Return the list of available hvac operation modes.""" return [HVACMode.OFF] + [ - ATA_HVAC_MODE_LOOKUP.get(mode) for mode in self._device.operation_modes + ATA_HVAC_MODE_LOOKUP[mode] + for mode in self._device.operation_modes + if mode in ATA_HVAC_MODE_LOOKUP ] @property @@ -201,9 +208,9 @@ class AtaDeviceClimate(MelCloudClimate): """Return the temperature we try to reach.""" return self._device.target_temperature - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - set_dict = {} + set_dict: dict[str, Any] = {} if ATTR_HVAC_MODE in kwargs: self._apply_set_hvac_mode( kwargs.get(ATTR_HVAC_MODE, self.hvac_mode), set_dict @@ -255,7 +262,7 @@ class AtaDeviceClimate(MelCloudClimate): await self.async_set_vane_vertical(swing_mode) @property - def swing_modes(self) -> str | None: + def swing_modes(self) -> list[str] | None: """Return a list of available vertical vane positions and modes.""" return self._device.vane_vertical_positions diff --git a/homeassistant/components/melcloud/translations/ja.json b/homeassistant/components/melcloud/translations/ja.json index b8f2d14e5cd..377ba291786 100644 --- a/homeassistant/components/melcloud/translations/ja.json +++ b/homeassistant/components/melcloud/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3053\u306e\u30e1\u30fc\u30eb\u306b\u306f\u3059\u3067\u306b\u3001MELCloud\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002" + "already_configured": "\u3053\u306e\u30e1\u30fc\u30eb\u306b\u306f\u3059\u3067\u306b\u3001MELCloud\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 2f663df458e..b4889a62411 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 0ff0a60bfa1..c843be73fe7 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -71,6 +71,7 @@ def format_condition(condition: str) -> str: class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" + _attr_has_entity_name = True _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_precipitation_unit = LENGTH_MILLIMETERS _attr_native_pressure_unit = PRESSURE_HPA @@ -111,7 +112,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): name = self._config.get(CONF_NAME) name_appendix = "" if self._hourly: - name_appendix = " Hourly" + name_appendix = " hourly" if name is not None: return f"{name}{name_appendix}" diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py index 399e72d4924..a5b096b5554 100644 --- a/homeassistant/components/met_eireann/__init__.py +++ b/homeassistant/components/met_eireann/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/met_eireann/translations/pt.json b/homeassistant/components/met_eireann/translations/pt.json new file mode 100644 index 00000000000..785a126c56b --- /dev/null +++ b/homeassistant/components/met_eireann/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "name": "Nome" + }, + "title": "Localiza\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index bbc3b16875d..1d36746413e 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index df667b1964b..d6cbd34d88d 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -183,7 +183,7 @@ SENSOR_TYPES_PROBABILITY: tuple[MeteoFranceSensorEntityDescription, ...] = ( ) -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire"], ATTR_CONDITION_CLOUDY: ["Très nuageux", "Couvert"], ATTR_CONDITION_FOG: [ diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 0d90e4d33be..823018f405f 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,4 +1,6 @@ """Support for Meteo-France raining forecast sensor.""" +from __future__ import annotations + from meteofrance_api.helpers import ( get_warning_text_status_from_indice_color, readeable_phenomenoms_dict, @@ -97,6 +99,11 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" + assert ( + self.platform + and self.platform.config_entry + and self.platform.config_entry.unique_id + ) return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, @@ -191,7 +198,7 @@ class MeteoFranceAlertSensor(MeteoFranceSensor): def _find_first_probability_forecast_not_null( probability_forecast: list, path: list -) -> int: +) -> int | None: """Search the first not None value in the first forecast elements.""" for forecast in probability_forecast[0:3]: if forecast[path[1]][path[2]] is not None: diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index a30a65304b0..2727b4470c1 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -102,6 +102,11 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" + assert ( + self.platform + and self.platform.config_entry + and self.platform.config_entry.unique_id + ) return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 35333f6ea01..9a3da54d34f 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -2,7 +2,7 @@ "domain": "meteoalarm", "name": "MeteoAlarm", "documentation": "https://www.home-assistant.io/integrations/meteoalarm", - "requirements": ["meteoalertapi==0.2.0"], + "requirements": ["meteoalertapi==0.3.0"], "codeowners": ["@rolfberkenbosch"], "iot_class": "cloud_polling", "loggers": ["meteoalertapi"] diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 58e51d0490a..7510c4bec4c 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/meteoclimatic/translations/pt.json b/homeassistant/components/meteoclimatic/translations/pt.json new file mode 100644 index 00000000000..245d9637441 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 3d10fdef378..e71c417da43 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: metoffice_daily_coordinator.async_config_entry_first_refresh(), ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py index 959680a90ec..3cf3b0fcda0 100644 --- a/homeassistant/components/metoffice/config_flow.py +++ b/homeassistant/components/metoffice/config_flow.py @@ -1,11 +1,15 @@ """Config flow for Met Office integration.""" +from __future__ import annotations + import logging +from typing import Any import datapoint import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -14,7 +18,9 @@ from .helpers import fetch_site _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate that the user input allows us to connect to DataPoint. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -40,7 +46,9 @@ class MetOfficeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index e413b102898..12f88cc6d56 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -37,7 +37,7 @@ MODE_3HOURLY_LABEL = "3-Hourly" MODE_DAILY = "daily" MODE_DAILY_LABEL = "Daily" -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLEAR_NIGHT: ["0"], ATTR_CONDITION_CLOUDY: ["7", "8"], ATTR_CONDITION_FOG: ["5", "6"], diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py index 607c09e90b6..4b2741ce0fb 100644 --- a/homeassistant/components/metoffice/data.py +++ b/homeassistant/components/metoffice/data.py @@ -1,11 +1,17 @@ """Common Met Office Data class used by both sensor and entity.""" +from dataclasses import dataclass + +from datapoint.Forecast import Forecast +from datapoint.Site import Site +from datapoint.Timestep import Timestep + + +@dataclass class MetOfficeData: """Data structure for MetOffice weather and forecast.""" - def __init__(self, now, forecast, site): - """Initialize the data object.""" - self.now = now - self.forecast = forecast - self.site = site + now: Forecast + forecast: list[Timestep] + site: Site diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 00d5e73501d..ecef7e5ddcb 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -1,8 +1,10 @@ """Helpers used for Met Office integration.""" +from __future__ import annotations import logging import datapoint +from datapoint.Site import Site from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.util.dt import utcnow @@ -13,7 +15,9 @@ from .data import MetOfficeData _LOGGER = logging.getLogger(__name__) -def fetch_site(connection: datapoint.Manager, latitude, longitude): +def fetch_site( + connection: datapoint.Manager, latitude: float, longitude: float +) -> Site | None: """Fetch site information from Datapoint API.""" try: return connection.get_nearest_forecast_site( @@ -24,7 +28,7 @@ def fetch_site(connection: datapoint.Manager, latitude, longitude): return None -def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: +def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOfficeData: """Fetch weather and forecast from Datapoint API.""" try: forecast = connection.get_forecast_for_site(site.id, mode) @@ -34,8 +38,8 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: else: time_now = utcnow() return MetOfficeData( - forecast.now(), - [ + now=forecast.now(), + forecast=[ timestep for day in forecast.days for timestep in day.timesteps @@ -44,5 +48,5 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: mode == MODE_3HOURLY or timestep.date.hour > 6 ) # ensures only one result per day in MODE_DAILY ], - site, + site=site, ) diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index d38d2d8cffe..887ecb3578d 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -3,7 +3,7 @@ "name": "Met Office", "documentation": "https://www.home-assistant.io/integrations/metoffice", "requirements": ["datapoint==0.9.8"], - "codeowners": ["@MrHarcombe"], + "codeowners": ["@MrHarcombe", "@avee87"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["datapoint"] diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 699b137c55f..e24e2299be4 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -1,6 +1,10 @@ """Support for UK Met Office weather service.""" from __future__ import annotations +from typing import Any + +from datapoint.Element import Element + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -17,7 +21,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import get_device_info from .const import ( @@ -34,6 +41,7 @@ from .const import ( VISIBILITY_CLASSES, VISIBILITY_DISTANCE_CLASSES, ) +from .data import MetOfficeData ATTR_LAST_UPDATE = "last_update" ATTR_SENSOR_ID = "sensor_id" @@ -170,21 +178,24 @@ async def async_setup_entry( ) -class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): +class MetOfficeCurrentSensor( + CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], SensorEntity +): """Implementation of a Met Office current weather condition sensor.""" def __init__( self, - coordinator, - hass_data, - use_3hourly, + coordinator: DataUpdateCoordinator[MetOfficeData], + hass_data: dict[str, Any], + use_3hourly: bool, description: SensorEntityDescription, - ): + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL + self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) @@ -192,11 +203,12 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{description.name}_{hass_data[METOFFICE_COORDINATES]}" if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" - - self.use_3hourly = use_3hourly + self._attr_entity_registry_enabled_default = ( + self.entity_description.entity_registry_enabled_default and use_3hourly + ) @property - def native_value(self): + def native_value(self) -> Any | None: """Return the state of the sensor.""" value = None @@ -224,13 +236,13 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): elif hasattr(self.coordinator.data.now, self.entity_description.key): value = getattr(self.coordinator.data.now, self.entity_description.key) - if hasattr(value, "value"): + if isinstance(value, Element): value = value.value return value @property - def icon(self): + def icon(self) -> str | None: """Return the icon for the entity card.""" value = self.entity_description.icon if self.entity_description.key == "weather": @@ -244,7 +256,7 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): return value @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, @@ -253,10 +265,3 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): ATTR_SITE_ID: self.coordinator.data.site.id, ATTR_SITE_NAME: self.coordinator.data.site.name, } - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return ( - self.entity_description.entity_registry_enabled_default and self.use_3hourly - ) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index f4e0bf61d30..184782d4c12 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,18 +1,27 @@ """Support for UK Met Office weather service.""" +from __future__ import annotations + +from typing import Any + +from datapoint.Timestep import Timestep + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import get_device_info from .const import ( @@ -28,6 +37,7 @@ from .const import ( MODE_DAILY, MODE_DAILY_LABEL, ) +from .data import MetOfficeData async def async_setup_entry( @@ -45,9 +55,8 @@ async def async_setup_entry( ) -def _build_forecast_data(timestep): - data = {} - data[ATTR_FORECAST_TIME] = timestep.date.isoformat() +def _build_forecast_data(timestep: Timestep) -> Forecast: + data = Forecast(datetime=timestep.date.isoformat()) if timestep.weather: data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value) if timestep.precipitation: @@ -61,21 +70,30 @@ def _build_forecast_data(timestep): return data -def _get_weather_condition(metoffice_code): +def _get_weather_condition(metoffice_code: str) -> str | None: for hass_name, metoffice_codes in CONDITION_CLASSES.items(): if metoffice_code in metoffice_codes: return hass_name return None -class MetOfficeWeather(CoordinatorEntity, WeatherEntity): +class MetOfficeWeather( + CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], WeatherEntity +): """Implementation of a Met Office weather condition.""" + _attr_attribution = ATTRIBUTION + _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_pressure_unit = PRESSURE_HPA _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR - def __init__(self, coordinator, hass_data, use_3hourly): + def __init__( + self, + coordinator: DataUpdateCoordinator[MetOfficeData], + hass_data: dict[str, Any], + use_3hourly: bool, + ) -> None: """Initialise the platform with a data instance.""" super().__init__(coordinator) @@ -89,62 +107,61 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" if self.coordinator.data.now: return _get_weather_condition(self.coordinator.data.now.weather.value) return None @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the platform temperature.""" - if self.coordinator.data.now.temperature: - return self.coordinator.data.now.temperature.value + weather_now = self.coordinator.data.now + if weather_now.temperature: + value = weather_now.temperature.value + return float(value) if value is not None else None return None @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the mean sea-level pressure.""" weather_now = self.coordinator.data.now if weather_now and weather_now.pressure: - return weather_now.pressure.value + value = weather_now.pressure.value + return float(value) if value is not None else None return None @property - def humidity(self): + def humidity(self) -> float | None: """Return the relative humidity.""" weather_now = self.coordinator.data.now if weather_now and weather_now.humidity: - return weather_now.humidity.value + value = weather_now.humidity.value + return float(value) if value is not None else None return None @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the wind speed.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_speed: - return weather_now.wind_speed.value + value = weather_now.wind_speed.value + return float(value) if value is not None else None return None @property - def wind_bearing(self): + def wind_bearing(self) -> str | None: """Return the wind bearing.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_direction: - return weather_now.wind_direction.value + value = weather_now.wind_direction.value + return str(value) if value is not None else None return None @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - if self.coordinator.data.forecast is None: - return None return [ _build_forecast_data(timestep) for timestep in self.coordinator.data.forecast ] - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index eea4b2b82fe..faae8fb140e 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -2,8 +2,8 @@ "domain": "miflora", "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", - "requirements": ["bluepy==1.3.0", "miflora==0.7.2"], + "requirements": [], + "dependencies": ["repairs"], "codeowners": ["@danielhiversen", "@basnijholt"], - "iot_class": "local_polling", - "loggers": ["btlewrap", "miflora"] + "iot_class": "local_polling" } diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 2dd819e45c5..76808e9706c 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -1,113 +1,13 @@ """Support for Xiaomi Mi Flora BLE plant sensor.""" from __future__ import annotations -from datetime import timedelta -import logging -from typing import Any - -import btlewrap -from btlewrap import BluetoothBackendException -from miflora import miflora_poller -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONDUCTIVITY, - CONF_FORCE_UPDATE, - CONF_MAC, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, - LIGHT_LUX, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.dt as dt_util -try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - - BACKEND = btlewrap.BluepyBackend -except ImportError: - BACKEND = btlewrap.GatttoolBackend - -_LOGGER = logging.getLogger(__name__) - -CONF_ADAPTER = "adapter" -CONF_MEDIAN = "median" -CONF_GO_UNAVAILABLE_TIMEOUT = "go_unavailable_timeout" - -DEFAULT_ADAPTER = "hci0" -DEFAULT_FORCE_UPDATE = False -DEFAULT_MEDIAN = 3 -DEFAULT_NAME = "Mi Flora" -DEFAULT_GO_UNAVAILABLE_TIMEOUT = timedelta(seconds=7200) - -SCAN_INTERVAL = timedelta(seconds=1200) - -ATTR_LAST_SUCCESSFUL_UPDATE = "last_successful_update" - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="temperature", - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key="light", - name="Light intensity", - native_unit_of_measurement=LIGHT_LUX, - device_class=SensorDeviceClass.ILLUMINANCE, - ), - SensorEntityDescription( - key="moisture", - name="Moisture", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:water-percent", - ), - SensorEntityDescription( - key="conductivity", - name="Conductivity", - native_unit_of_measurement=CONDUCTIVITY, - icon="mdi:lightning-bolt-circle", - ), - SensorEntityDescription( - key="battery", - name="Battery", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.BATTERY, - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, - vol.Optional( - CONF_GO_UNAVAILABLE_TIMEOUT, default=DEFAULT_GO_UNAVAILABLE_TIMEOUT - ): cv.time_period, - } -) +PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE async def async_setup_platform( @@ -117,125 +17,13 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the MiFlora sensor.""" - backend = BACKEND - _LOGGER.debug("Miflora is using %s backend", backend.__name__) - - cache = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL).total_seconds() - poller = miflora_poller.MiFloraPoller( - config[CONF_MAC], - cache_timeout=cache, - adapter=config[CONF_ADAPTER], - backend=backend, + async_create_issue( + hass, + "miflora", + "replaced", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="replaced", + learn_more_url="https://www.home-assistant.io/integrations/xiaomi_ble/", ) - force_update = config[CONF_FORCE_UPDATE] - median = config[CONF_MEDIAN] - - go_unavailable_timeout = config[CONF_GO_UNAVAILABLE_TIMEOUT] - - prefix = config[CONF_NAME] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - MiFloraSensor( - description, - poller, - prefix, - force_update, - median, - go_unavailable_timeout, - ) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - async_add_entities(entities) - - -class MiFloraSensor(SensorEntity): - """Implementing the MiFlora sensor.""" - - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__( - self, - description: SensorEntityDescription, - poller, - prefix, - force_update, - median, - go_unavailable_timeout, - ): - """Initialize the sensor.""" - self.entity_description = description - self.poller = poller - self.data: list[Any] = [] - if prefix: - self._attr_name = f"{prefix} {description.name}" - self._attr_force_update = force_update - self.go_unavailable_timeout = go_unavailable_timeout - self.last_successful_update = dt_util.utc_from_timestamp(0) - # Median is used to filter out outliers. median of 3 will filter - # single outliers, while median of 5 will filter double outliers - # Use median_count = 1 if no filtering is required. - self.median_count = median - - async def async_added_to_hass(self): - """Set initial state.""" - - @callback - def on_startup(_): - self.async_schedule_update_ha_state(True) - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, on_startup) - - @property - def available(self): - """Return True if did update since 2h.""" - return self.last_successful_update > ( - dt_util.utcnow() - self.go_unavailable_timeout - ) - - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return {ATTR_LAST_SUCCESSFUL_UPDATE: self.last_successful_update} - - def update(self): - """ - Update current conditions. - - This uses a rolling median over 3 values to filter out outliers. - """ - try: - _LOGGER.debug("Polling data for %s", self.name) - data = self.poller.parameter_value(self.entity_description.key) - except (OSError, BluetoothBackendException) as err: - _LOGGER.info("Polling error %s: %s", type(err).__name__, err) - return - - if data is not None: - _LOGGER.debug("%s = %s", self.name, data) - self.data.append(data) - self.last_successful_update = dt_util.utcnow() - else: - _LOGGER.info("Did not receive any data from Mi Flora sensor %s", self.name) - # Remove old data from median list or set sensor value to None - # if no data is available anymore - if self.data: - self.data = self.data[1:] - else: - self._attr_native_value = None - return - - _LOGGER.debug("Data collected: %s", self.data) - if len(self.data) > self.median_count: - self.data = self.data[1:] - - if len(self.data) == self.median_count: - median = sorted(self.data)[int((self.median_count - 1) / 2)] - _LOGGER.debug("Median is: %s", median) - self._attr_native_value = median - elif self._attr_native_value is None: - _LOGGER.debug("Set initial state") - self._attr_native_value = self.data[0] - else: - _LOGGER.debug("Not yet enough data for median calculation") diff --git a/homeassistant/components/miflora/strings.json b/homeassistant/components/miflora/strings.json new file mode 100644 index 00000000000..03427e88af9 --- /dev/null +++ b/homeassistant/components/miflora/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "title": "The Mi Flora integration has been replaced", + "description": "The Mi Flora integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Mi Flora device using the new integration manually.\n\nYour existing Mi Flora YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/miflora/translations/de.json b/homeassistant/components/miflora/translations/de.json new file mode 100644 index 00000000000..8120ddcd25a --- /dev/null +++ b/homeassistant/components/miflora/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Die Mi Flora-Integration funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE-Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Mi Flora-Ger\u00e4t mit der neuen Integration manuell hinzuf\u00fcgen.\n\nDeine bestehende Mi Flora YAML-Konfiguration wird vom Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Mi Flora-Integration wurde ersetzt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/el.json b/homeassistant/components/miflora/translations/el.json new file mode 100644 index 00000000000..bf4076979d1 --- /dev/null +++ b/homeassistant/components/miflora/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Mi Flora \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant 2022.7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Xiaomi BLE \u03c3\u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 2022.8. \n\n \u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 Mi Flora \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Mi Flora YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Mi Flora \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/en.json b/homeassistant/components/miflora/translations/en.json new file mode 100644 index 00000000000..52a8a71594c --- /dev/null +++ b/homeassistant/components/miflora/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "The Mi Flora integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Mi Flora device using the new integration manually.\n\nYour existing Mi Flora YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Mi Flora integration has been replaced" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/it.json b/homeassistant/components/miflora/translations/it.json new file mode 100644 index 00000000000..cdd2d89ca72 --- /dev/null +++ b/homeassistant/components/miflora/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "L'integrazione Mi Flora ha smesso di funzionare in Home Assistant 2022.7 e sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Mi Flora utilizzando la nuova integrazione. \n\nLa configurazione YAML di Mi Flora esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione Mi Flora \u00e8 stata sostituita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/pl.json b/homeassistant/components/miflora/translations/pl.json new file mode 100644 index 00000000000..d8b80bdd26d --- /dev/null +++ b/homeassistant/components/miflora/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integracja Mi Flora przesta\u0142a dzia\u0142a\u0107 w Home Assistant 2022.7 i zosta\u0142a zast\u0105piona integracj\u0105 Xiaomi BLE w wydaniu 2022.8. \n\nNie ma mo\u017cliwo\u015bci migracji, dlatego musisz r\u0119cznie doda\u0107 urz\u0105dzenie Mi Flora za pomoc\u0105 nowej integracji. \n\nTwoja istniej\u0105ca konfiguracja YAML dla Mi Flora nie jest ju\u017c u\u017cywana przez Home Assistanta. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Mi Flora zosta\u0142a zast\u0105piona" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/pt-BR.json b/homeassistant/components/miflora/translations/pt-BR.json new file mode 100644 index 00000000000..3b8b4658040 --- /dev/null +++ b/homeassistant/components/miflora/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A integra\u00e7\u00e3o do Mi Flora parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Mi Flora usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o existente do Mi Flora YAML n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do Mi Flora foi substitu\u00edda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/zh-Hant.json b/homeassistant/components/miflora/translations/zh-Hant.json new file mode 100644 index 00000000000..e6af26efcb9 --- /dev/null +++ b/homeassistant/components/miflora/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u82b1\u82b1\u8349\u8349 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 856495dc0f2..f72c79c1559 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,35 +1,41 @@ """The Mikrotik component.""" from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ATTR_MANUFACTURER, DOMAIN, PLATFORMS -from .hub import MikrotikDataUpdateCoordinator +from .errors import CannotConnect, LoginError +from .hub import MikrotikDataUpdateCoordinator, get_api CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Mikrotik component.""" - - hub = MikrotikDataUpdateCoordinator(hass, config_entry) - if not await hub.async_setup(): + try: + api = await hass.async_add_executor_job(get_api, dict(config_entry.data)) + except CannotConnect as api_error: + raise ConfigEntryNotReady from api_error + except LoginError: return False - await hub.async_config_entry_first_refresh() + coordinator = MikrotikDataUpdateCoordinator(hass, config_entry, api) + await hass.async_add_executor_job(coordinator.api.get_hub_details) + await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(DOMAIN, hub.serial_num)}, + connections={(DOMAIN, coordinator.serial_num)}, manufacturer=ATTR_MANUFACTURER, - model=hub.model, - name=hub.hostname, - sw_version=hub.firmware, + model=coordinator.model, + name=coordinator.hostname, + sw_version=coordinator.firmware, ) return True @@ -37,10 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( + if unload_ok := await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS - ) - - hass.data[DOMAIN].pop(config_entry.entry_id) + ): + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index 36b65b6f2ba..d506c2c75e4 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Mikrotik.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -13,6 +15,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_ARP_PING, @@ -40,7 +43,9 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return MikrotikOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -52,7 +57,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): break try: - await self.hass.async_add_executor_job(get_api, self.hass, user_input) + await self.hass.async_add_executor_job(get_api, user_input) except CannotConnect: errors["base"] = "cannot_connect" except LoginError: @@ -86,11 +91,15 @@ class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Mikrotik options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Mikrotik options.""" return await self.async_step_device_tracker() - async def async_step_device_tracker(self, user_input=None): + async def async_step_device_tracker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the device tracker options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index b328c10a602..bbe129c4a00 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -1,33 +1,35 @@ """Constants used in the Mikrotik components.""" +from typing import Final + from homeassistant.const import Platform -DOMAIN = "mikrotik" -DEFAULT_NAME = "Mikrotik" -DEFAULT_API_PORT = 8728 -DEFAULT_DETECTION_TIME = 300 +DOMAIN: Final = "mikrotik" +DEFAULT_NAME: Final = "Mikrotik" +DEFAULT_API_PORT: Final = 8728 +DEFAULT_DETECTION_TIME: Final = 300 -ATTR_MANUFACTURER = "Mikrotik" -ATTR_SERIAL_NUMBER = "serial-number" -ATTR_FIRMWARE = "current-firmware" -ATTR_MODEL = "model" +ATTR_MANUFACTURER: Final = "Mikrotik" +ATTR_SERIAL_NUMBER: Final = "serial-number" +ATTR_FIRMWARE: Final = "current-firmware" +ATTR_MODEL: Final = "model" -CONF_ARP_PING = "arp_ping" -CONF_FORCE_DHCP = "force_dhcp" -CONF_DETECTION_TIME = "detection_time" +CONF_ARP_PING: Final = "arp_ping" +CONF_FORCE_DHCP: Final = "force_dhcp" +CONF_DETECTION_TIME: Final = "detection_time" -NAME = "name" -INFO = "info" -IDENTITY = "identity" -ARP = "arp" +NAME: Final = "name" +INFO: Final = "info" +IDENTITY: Final = "identity" +ARP: Final = "arp" -CAPSMAN = "capsman" -DHCP = "dhcp" -WIRELESS = "wireless" -IS_WIRELESS = "is_wireless" -IS_CAPSMAN = "is_capsman" +CAPSMAN: Final = "capsman" +DHCP: Final = "dhcp" +WIRELESS: Final = "wireless" +IS_WIRELESS: Final = "is_wireless" +IS_CAPSMAN: Final = "is_capsman" -MIKROTIK_SERVICES = { +MIKROTIK_SERVICES: Final = { ARP: "/ip/arp/getall", CAPSMAN: "/caps-man/registration-table/getall", DHCP: "/ip/dhcp-server/lease/getall", @@ -38,11 +40,10 @@ MIKROTIK_SERVICES = { IS_CAPSMAN: "/caps-man/interface/print", } -PLATFORMS = [Platform.DEVICE_TRACKER] +PLATFORMS: Final = [Platform.DEVICE_TRACKER] -ATTR_DEVICE_TRACKER = [ +ATTR_DEVICE_TRACKER: Final = [ "comment", - "mac-address", "ssid", "interface", "signal-strength", diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 9389d3bea5c..158d95dd683 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -1,6 +1,8 @@ """Support for Mikrotik routers as device tracker.""" from __future__ import annotations +from typing import Any + from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER, @@ -14,11 +16,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.dt as dt_util from .const import DOMAIN -from .hub import MikrotikDataUpdateCoordinator - -# These are normalized to ATTR_IP and ATTR_MAC to conform -# to device_tracker -FILTER_ATTRS = ("ip_address", "mac_address") +from .hub import Device, MikrotikDataUpdateCoordinator async def async_setup_entry( @@ -27,7 +25,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up device tracker for Mikrotik component.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + coordinator: MikrotikDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] tracked: dict[str, MikrotikDataUpdateCoordinatorTracker] = {} @@ -42,47 +42,55 @@ async def async_setup_entry( ): if ( - entity.unique_id in hub.api.devices - or entity.unique_id not in hub.api.all_devices + entity.unique_id in coordinator.api.devices + or entity.unique_id not in coordinator.api.all_devices ): continue - hub.api.restore_device(entity.unique_id) + coordinator.api.restore_device(entity.unique_id) @callback - def update_hub(): + def update_hub() -> None: """Update the status of the device.""" - update_items(hub, async_add_entities, tracked) + update_items(coordinator, async_add_entities, tracked) - config_entry.async_on_unload(hub.async_add_listener(update_hub)) + config_entry.async_on_unload(coordinator.async_add_listener(update_hub)) update_hub() @callback -def update_items(hub, async_add_entities, tracked): +def update_items( + coordinator: MikrotikDataUpdateCoordinator, + async_add_entities: AddEntitiesCallback, + tracked: dict[str, MikrotikDataUpdateCoordinatorTracker], +): """Update tracked device state from the hub.""" - new_tracked = [] - for mac, device in hub.api.devices.items(): + new_tracked: list[MikrotikDataUpdateCoordinatorTracker] = [] + for mac, device in coordinator.api.devices.items(): if mac not in tracked: - tracked[mac] = MikrotikDataUpdateCoordinatorTracker(device, hub) + tracked[mac] = MikrotikDataUpdateCoordinatorTracker(device, coordinator) new_tracked.append(tracked[mac]) if new_tracked: async_add_entities(new_tracked) -class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): +class MikrotikDataUpdateCoordinatorTracker( + CoordinatorEntity[MikrotikDataUpdateCoordinator], ScannerEntity +): """Representation of network device.""" - coordinator: MikrotikDataUpdateCoordinator - - def __init__(self, device, hub): + def __init__( + self, device: Device, coordinator: MikrotikDataUpdateCoordinator + ) -> None: """Initialize the tracked device.""" - super().__init__(hub) + super().__init__(coordinator) self.device = device + self._attr_name = str(device.name) + self._attr_unique_id = device.mac @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the client is connected to the network.""" if ( self.device.last_seen @@ -93,16 +101,10 @@ class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): return False @property - def source_type(self): + def source_type(self) -> str: """Return the source type of the client.""" return SOURCE_TYPE_ROUTER - @property - def name(self) -> str: - """Return the name of the client.""" - # Stringify to ensure we return a string - return str(self.device.name) - @property def hostname(self) -> str: """Return the hostname of the client.""" @@ -119,13 +121,6 @@ class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): return self.device.ip_address @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return self.device.mac - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" - if self.is_connected: - return {k: v for k, v in self.device.attrs.items() if k not in FILTER_ATTRS} - return None + return self.device.attrs if self.is_connected else None diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 7f2314bd057..66fe7226d9b 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -1,14 +1,18 @@ """The Mikrotik router class.""" -from datetime import timedelta +from __future__ import annotations + +from datetime import datetime, timedelta import logging import socket import ssl +from typing import Any import librouteros from librouteros.login import plain as login_plain, token as login_token +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -42,45 +46,49 @@ _LOGGER = logging.getLogger(__name__) class Device: """Represents a network device.""" - def __init__(self, mac, params): + def __init__(self, mac: str, params: dict[str, Any]) -> None: """Initialize the network device.""" self._mac = mac self._params = params - self._last_seen = None - self._attrs = {} - self._wireless_params = None + self._last_seen: datetime | None = None + self._attrs: dict[str, Any] = {} + self._wireless_params: dict[str, Any] = {} @property - def name(self): + def name(self) -> str: """Return device name.""" return self._params.get("host-name", self.mac) @property - def ip_address(self): + def ip_address(self) -> str: """Return device primary ip address.""" - return self._params.get("address") + return self._params["address"] @property - def mac(self): + def mac(self) -> str: """Return device mac.""" return self._mac @property - def last_seen(self): + def last_seen(self) -> datetime | None: """Return device last seen.""" return self._last_seen @property - def attrs(self): + def attrs(self) -> dict[str, Any]: """Return device attributes.""" - attr_data = self._wireless_params if self._wireless_params else self._params + attr_data = self._wireless_params | self._params for attr in ATTR_DEVICE_TRACKER: if attr in attr_data: self._attrs[slugify(attr)] = attr_data[attr] - self._attrs["ip_address"] = self._params.get("active-address") return self._attrs - def update(self, wireless_params=None, params=None, active=False): + def update( + self, + wireless_params: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + active: bool = False, + ) -> None: """Update Device params.""" if wireless_params: self._wireless_params = wireless_params @@ -93,27 +101,26 @@ class Device: class MikrotikData: """Handle all communication with the Mikrotik API.""" - def __init__(self, hass, config_entry, api): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: librouteros.Api + ) -> None: """Initialize the Mikrotik Client.""" self.hass = hass self.config_entry = config_entry self.api = api - self._host = self.config_entry.data[CONF_HOST] - self.all_devices = {} - self.devices = {} - self.available = True - self.support_capsman = False - self.support_wireless = False - self.hostname = None - self.model = None - self.firmware = None - self.serial_number = None + self._host: str = self.config_entry.data[CONF_HOST] + self.all_devices: dict[str, dict[str, Any]] = {} + self.devices: dict[str, Device] = {} + self.support_capsman: bool = False + self.support_wireless: bool = False + self.hostname: str = "" + self.model: str = "" + self.firmware: str = "" + self.serial_number: str = "" @staticmethod - def load_mac(devices=None): + def load_mac(devices: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: """Load dictionary using MAC address as key.""" - if not devices: - return None mac_devices = {} for device in devices: if "mac-address" in device: @@ -122,26 +129,23 @@ class MikrotikData: return mac_devices @property - def arp_enabled(self): + def arp_enabled(self) -> bool: """Return arp_ping option setting.""" - return self.config_entry.options[CONF_ARP_PING] + return self.config_entry.options.get(CONF_ARP_PING, False) @property - def force_dhcp(self): + def force_dhcp(self) -> bool: """Return force_dhcp option setting.""" - return self.config_entry.options[CONF_FORCE_DHCP] + return self.config_entry.options.get(CONF_FORCE_DHCP, False) - def get_info(self, param): + def get_info(self, param: str) -> str: """Return device model name.""" cmd = IDENTITY if param == NAME else INFO - data = self.command(MIKROTIK_SERVICES[cmd]) - return ( - data[0].get(param) # pylint: disable=unsubscriptable-object - if data - else None - ) + if data := self.command(MIKROTIK_SERVICES[cmd]): + return str(data[0].get(param)) + return "" - def get_hub_details(self): + def get_hub_details(self) -> None: """Get Hub info.""" self.hostname = self.get_info(NAME) self.model = self.get_info(ATTR_MODEL) @@ -150,24 +154,17 @@ class MikrotikData: self.support_capsman = bool(self.command(MIKROTIK_SERVICES[IS_CAPSMAN])) self.support_wireless = bool(self.command(MIKROTIK_SERVICES[IS_WIRELESS])) - def connect_to_hub(self): - """Connect to hub.""" - try: - self.api = get_api(self.hass, self.config_entry.data) - return True - except (LoginError, CannotConnect): - return False - - def get_list_from_interface(self, interface): + def get_list_from_interface(self, interface: str) -> dict[str, dict[str, Any]]: """Get devices from interface.""" - result = self.command(MIKROTIK_SERVICES[interface]) - return self.load_mac(result) if result else {} + if result := self.command(MIKROTIK_SERVICES[interface]): + return self.load_mac(result) + return {} - def restore_device(self, mac): + def restore_device(self, mac: str) -> None: """Restore a missing device after restart.""" self.devices[mac] = Device(mac, self.all_devices[mac]) - def update_devices(self): + def update_devices(self) -> None: """Get list of devices with latest status.""" arp_devices = {} device_list = {} @@ -192,7 +189,7 @@ class MikrotikData: # get new hub firmware version if updated self.firmware = self.get_info(ATTR_FIRMWARE) - except (CannotConnect, socket.timeout, OSError) as err: + except (CannotConnect, LoginError) as err: raise UpdateFailed from err if not device_list: @@ -218,11 +215,12 @@ class MikrotikData: active = True if self.arp_enabled and mac in arp_devices: active = self.do_arp_ping( - params.get("active-address"), arp_devices[mac].get("interface") + str(params.get("active-address")), + str(arp_devices[mac].get("interface")), ) self.devices[mac].update(active=active) - def do_arp_ping(self, ip_address, interface): + def do_arp_ping(self, ip_address: str, interface: str) -> bool: """Attempt to arp ping MAC address via interface.""" _LOGGER.debug("pinging - %s", ip_address) params = { @@ -234,9 +232,9 @@ class MikrotikData: } cmd = "/ping" data = self.command(cmd, params) - if data is not None: + if data: status = 0 - for result in data: # pylint: disable=not-an-iterable + for result in data: if "status" in result: status += 1 if status == len(data): @@ -246,22 +244,25 @@ class MikrotikData: return False return True - def command(self, cmd, params=None): + def command( + self, cmd: str, params: dict[str, Any] | None = None + ) -> list[dict[str, Any]]: """Retrieve data from Mikrotik API.""" try: - _LOGGER.info("Running command %s", cmd) + _LOGGER.debug("Running command %s", cmd) if params: - response = list(self.api(cmd=cmd, **params)) - else: - response = list(self.api(cmd=cmd)) + return list(self.api(cmd=cmd, **params)) + return list(self.api(cmd=cmd)) except ( librouteros.exceptions.ConnectionClosed, OSError, socket.timeout, ) as api_error: _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) - if not self.connect_to_hub(): - raise CannotConnect from api_error + # try to reconnect + self.api = get_api(dict(self.config_entry.data)) + # we still have to raise CannotConnect to fail the update. + raise CannotConnect from api_error except librouteros.exceptions.ProtocolError as api_error: _LOGGER.warning( "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", @@ -269,106 +270,71 @@ class MikrotikData: cmd, api_error, ) - return None - - return response if response else None + return [] class MikrotikDataUpdateCoordinator(DataUpdateCoordinator): """Mikrotik Hub Object.""" - def __init__(self, hass, config_entry): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: librouteros.Api + ) -> None: """Initialize the Mikrotik Client.""" self.hass = hass - self.config_entry = config_entry - self._mk_data = None + self.config_entry: ConfigEntry = config_entry + self._mk_data = MikrotikData(self.hass, self.config_entry, api) super().__init__( self.hass, _LOGGER, name=f"{DOMAIN} - {self.host}", - update_method=self.async_update, update_interval=timedelta(seconds=10), ) @property - def host(self): + def host(self) -> str: """Return the host of this hub.""" return self.config_entry.data[CONF_HOST] @property - def hostname(self): + def hostname(self) -> str: """Return the hostname of the hub.""" return self._mk_data.hostname @property - def model(self): + def model(self) -> str: """Return the model of the hub.""" return self._mk_data.model @property - def firmware(self): + def firmware(self) -> str: """Return the firmware of the hub.""" return self._mk_data.firmware @property - def serial_num(self): + def serial_num(self) -> str: """Return the serial number of the hub.""" return self._mk_data.serial_number @property - def available(self): - """Return if the hub is connected.""" - return self._mk_data.available - - @property - def option_detection_time(self): + def option_detection_time(self) -> timedelta: """Config entry option defining number of seconds from last seen to away.""" - return timedelta(seconds=self.config_entry.options[CONF_DETECTION_TIME]) + return timedelta( + seconds=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + ) @property - def api(self): + def api(self) -> MikrotikData: """Represent Mikrotik data object.""" return self._mk_data - async def async_add_options(self): - """Populate default options for Mikrotik.""" - if not self.config_entry.options: - data = dict(self.config_entry.data) - options = { - CONF_ARP_PING: data.pop(CONF_ARP_PING, False), - CONF_FORCE_DHCP: data.pop(CONF_FORCE_DHCP, False), - CONF_DETECTION_TIME: data.pop( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ), - } - - self.hass.config_entries.async_update_entry( - self.config_entry, data=data, options=options - ) - - async def async_update(self): + async def _async_update_data(self) -> None: """Update Mikrotik devices information.""" await self.hass.async_add_executor_job(self._mk_data.update_devices) - async def async_setup(self): - """Set up the Mikrotik hub.""" - try: - api = await self.hass.async_add_executor_job( - get_api, self.hass, self.config_entry.data - ) - except CannotConnect as api_error: - raise ConfigEntryNotReady from api_error - except LoginError: - return False - self._mk_data = MikrotikData(self.hass, self.config_entry, api) - await self.async_add_options() - await self.hass.async_add_executor_job(self._mk_data.get_hub_details) - - return True - - -def get_api(hass, entry): +def get_api(entry: dict[str, Any]) -> librouteros.Api: """Connect to Mikrotik hub.""" _LOGGER.debug("Connecting to Mikrotik hub [%s]", entry[CONF_HOST]) diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 6131ebc7fd4..8fd1d1a3e22 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][conn_type][key] = data_coordinator await data_coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/min_max/__init__.py b/homeassistant/components/min_max/__init__.py index db80473f90a..a027a029ec2 100644 --- a/homeassistant/components/min_max/__init__.py +++ b/homeassistant/components/min_max/__init__.py @@ -9,7 +9,7 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Min/Max from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index b45cc0fb2e3..4abfbca9a2f 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -1,21 +1,22 @@ """The Minecraft Server integration.""" from __future__ import annotations +from collections.abc import Mapping from datetime import datetime, timedelta import logging +from typing import Any from mcstatus.server import MinecraftServer as MCStatus from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType from . import helpers from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX @@ -30,6 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: domain_data = hass.data.setdefault(DOMAIN, {}) # Create and store server instance. + assert entry.unique_id unique_id = entry.unique_id _LOGGER.debug( "Creating server instance for '%s' (%s)", @@ -42,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: server.start_periodic_update() # Set up platforms. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -71,7 +73,7 @@ class MinecraftServer: _MAX_RETRIES_STATUS = 3 def __init__( - self, hass: HomeAssistant, unique_id: str, config_data: ConfigType + self, hass: HomeAssistant, unique_id: str, config_data: Mapping[str, Any] ) -> None: """Initialize server instance.""" self._hass = hass @@ -94,14 +96,14 @@ class MinecraftServer: self.latency_time = None self.players_online = None self.players_max = None - self.players_list = None + self.players_list: list[str] | None = None self.motd = None # Dispatcher signal name self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" # Callback for stopping periodic update. - self._stop_periodic_update = None + self._stop_periodic_update: CALLBACK_TYPE | None = None def start_periodic_update(self) -> None: """Start periodic execution of update method.""" @@ -111,7 +113,8 @@ class MinecraftServer: def stop_periodic_update(self) -> None: """Stop periodic execution of update method.""" - self._stop_periodic_update() + if self._stop_periodic_update: + self._stop_periodic_update() async def async_check_connection(self) -> None: """Check server connection using a 'status' request and store connection status.""" @@ -219,14 +222,21 @@ class MinecraftServer: class MinecraftServerEntity(Entity): """Representation of a Minecraft Server base entity.""" + _attr_has_entity_name = True + _attr_should_poll = False + def __init__( - self, server: MinecraftServer, type_name: str, icon: str, device_class: str + self, + server: MinecraftServer, + type_name: str, + icon: str, + device_class: str | None, ) -> None: """Initialize base entity.""" self._server = server - self._name = f"{server.name} {type_name}" - self._icon = icon - self._unique_id = f"{self._server.unique_id}-{type_name}" + self._attr_name = type_name + self._attr_icon = icon + self._attr_unique_id = f"{self._server.unique_id}-{type_name}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._server.unique_id)}, manufacturer=MANUFACTURER, @@ -234,34 +244,9 @@ class MinecraftServerEntity(Entity): name=self._server.name, sw_version=self._server.protocol_version, ) - self._device_class = device_class + self._attr_device_class = device_class self._extra_state_attributes = None - self._disconnect_dispatcher = None - - @property - def name(self) -> str: - """Return name.""" - return self._name - - @property - def unique_id(self) -> str: - """Return unique ID.""" - return self._unique_id - - @property - def device_class(self) -> str: - """Return device class.""" - return self._device_class - - @property - def icon(self) -> str: - """Return icon.""" - return self._icon - - @property - def should_poll(self) -> bool: - """Disable polling.""" - return False + self._disconnect_dispatcher: CALLBACK_TYPE | None = None async def async_update(self) -> None: """Fetch data from the server.""" @@ -275,7 +260,8 @@ class MinecraftServerEntity(Entity): async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher before removal.""" - self._disconnect_dispatcher() + if self._disconnect_dispatcher: + self._disconnect_dispatcher() @callback def _update_callback(self) -> None: diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index 75c572f366f..0bf4cdab859 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -37,13 +37,8 @@ class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorEntit icon=ICON_STATUS, device_class=BinarySensorDeviceClass.CONNECTIVITY, ) - self._is_on = False - - @property - def is_on(self) -> bool: - """Return binary state.""" - return self._is_on + self._attr_is_on = False async def async_update(self) -> None: """Update status.""" - self._is_on = self._server.online + self._attr_is_on = self._server.online diff --git a/homeassistant/components/minecraft_server/helpers.py b/homeassistant/components/minecraft_server/helpers.py index 13ec4cd1afb..7153c170a6a 100644 --- a/homeassistant/components/minecraft_server/helpers.py +++ b/homeassistant/components/minecraft_server/helpers.py @@ -11,7 +11,9 @@ from homeassistant.core import HomeAssistant from .const import SRV_RECORD_PREFIX -async def async_check_srv_record(hass: HomeAssistant, host: str) -> dict[str, Any]: +async def async_check_srv_record( + hass: HomeAssistant, host: str +) -> dict[str, Any] | None: """Check if the given host is a valid Minecraft SRV record.""" # Check if 'host' is a valid SRV record. return_value = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index d7ca73d1411..f10a359eca0 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -1,8 +1,6 @@ """The Minecraft Server sensor platform.""" from __future__ import annotations -from typing import Any - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MILLISECONDS @@ -62,30 +60,19 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): self, server: MinecraftServer, type_name: str, - icon: str = None, - unit: str = None, - device_class: str = None, + icon: str, + unit: str | None, + device_class: str | None = None, ) -> None: """Initialize sensor base entity.""" super().__init__(server, type_name, icon, device_class) - self._state = None - self._unit = unit + self._attr_native_unit_of_measurement = unit @property def available(self) -> bool: """Return sensor availability.""" return self._server.online - @property - def native_value(self) -> Any: - """Return sensor state.""" - return self._state - - @property - def native_unit_of_measurement(self) -> str: - """Return sensor measurement unit.""" - return self._unit - class MinecraftServerVersionSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server version sensor.""" @@ -98,7 +85,7 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update version.""" - self._state = self._server.version + self._attr_native_value = self._server.version class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): @@ -115,7 +102,7 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update protocol version.""" - self._state = self._server.protocol_version + self._attr_native_value = self._server.protocol_version class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): @@ -132,7 +119,7 @@ class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update latency time.""" - self._state = self._server.latency_time + self._attr_native_value = self._server.latency_time class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): @@ -149,20 +136,15 @@ class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update online players state and device state attributes.""" - self._state = self._server.players_online + self._attr_native_value = self._server.players_online - extra_state_attributes = None + extra_state_attributes = {} players_list = self._server.players_list if players_list is not None and len(players_list) != 0: - extra_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list} + extra_state_attributes[ATTR_PLAYERS_LIST] = self._server.players_list - self._extra_state_attributes = extra_state_attributes - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return players list in device state attributes.""" - return self._extra_state_attributes + self._attr_extra_state_attributes = extra_state_attributes class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): @@ -179,7 +161,7 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update maximum number of players.""" - self._state = self._server.players_max + self._attr_native_value = self._server.players_max class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): @@ -196,4 +178,4 @@ class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update MOTD.""" - self._state = self._server.motd + self._attr_native_value = self._server.motd diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index 00fff153254..7c7232d46a0 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -4,9 +4,9 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa.", - "invalid_ip": "Az IP -c\u00edm \u00e9rv\u00e9nytelen (a MAC -c\u00edmet nem siker\u00fclt meghat\u00e1rozni). K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", - "invalid_port": "A portnak 1024 \u00e9s 65535 k\u00f6z\u00f6tt kell lennie. K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra." + "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa.", + "invalid_ip": "Az IP -c\u00edm \u00e9rv\u00e9nytelen (a MAC-c\u00edmet nem siker\u00fclt meghat\u00e1rozni). K\u00e9rem, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "invalid_port": "A portsz\u00e1mnak 1024 \u00e9s 65535 k\u00f6z\u00f6tt kell lennie. K\u00e9rem, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "step": { "user": { diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 07121b3695b..3af60301802 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -2,8 +2,8 @@ "domain": "mitemp_bt", "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", - "requirements": ["mitemp_bt==0.0.5"], + "requirements": [], + "dependencies": ["repairs"], "codeowners": [], - "iot_class": "local_polling", - "loggers": ["btlewrap", "mitemp_bt"] + "iot_class": "local_polling" } diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index e7f6237fdb1..74d0db7648c 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -1,188 +1,29 @@ """Support for Xiaomi Mi Temp BLE environmental sensor.""" from __future__ import annotations -import logging -from typing import Any - -import btlewrap -from btlewrap.base import BluetoothBackendException -from mitemp_bt import mitemp_bt_poller -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONF_FORCE_UPDATE, - CONF_MAC, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_TIMEOUT, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - - BACKEND = btlewrap.BluepyBackend -except ImportError: - BACKEND = btlewrap.GatttoolBackend - -_LOGGER = logging.getLogger(__name__) - -CONF_ADAPTER = "adapter" -CONF_CACHE = "cache_value" -CONF_MEDIAN = "median" -CONF_RETRIES = "retries" - -DEFAULT_ADAPTER = "hci0" -DEFAULT_UPDATE_INTERVAL = 300 -DEFAULT_FORCE_UPDATE = False -DEFAULT_MEDIAN = 3 -DEFAULT_NAME = "MiTemp BT" -DEFAULT_RETRIES = 2 -DEFAULT_TIMEOUT = 10 +PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="temperature", - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_CELSIUS, - ), - SensorEntityDescription( - key="humidity", - name="Humidity", - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="battery", - name="Battery", - device_class=SensorDeviceClass.BATTERY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ), -) - -SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, - vol.Optional(CONF_CACHE, default=DEFAULT_UPDATE_INTERVAL): cv.positive_int, - vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, - } -) - - -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the MiTempBt sensor.""" - backend = BACKEND - _LOGGER.debug("MiTempBt is using %s backend", backend.__name__) - - cache = config[CONF_CACHE] - poller = mitemp_bt_poller.MiTempBtPoller( - config[CONF_MAC], - cache_timeout=cache, - adapter=config[CONF_ADAPTER], - backend=backend, + async_create_issue( + hass, + "mitemp_bt", + "replaced", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="replaced", + learn_more_url="https://www.home-assistant.io/integrations/xiaomi_ble/", ) - prefix = config[CONF_NAME] - force_update = config[CONF_FORCE_UPDATE] - median = config[CONF_MEDIAN] - poller.ble_timeout = config[CONF_TIMEOUT] - poller.retries = config[CONF_RETRIES] - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - MiTempBtSensor(poller, prefix, force_update, median, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities) - - -class MiTempBtSensor(SensorEntity): - """Implementing the MiTempBt sensor.""" - - def __init__( - self, poller, prefix, force_update, median, description: SensorEntityDescription - ): - """Initialize the sensor.""" - self.entity_description = description - self.poller = poller - self.data: list[Any] = [] - self._attr_name = f"{prefix} {description.name}" - self._attr_force_update = force_update - # Median is used to filter out outliers. median of 3 will filter - # single outliers, while median of 5 will filter double outliers - # Use median_count = 1 if no filtering is required. - self.median_count = median - - def update(self): - """ - Update current conditions. - - This uses a rolling median over 3 values to filter out outliers. - """ - try: - _LOGGER.debug("Polling data for %s", self.name) - data = self.poller.parameter_value(self.entity_description.key) - except OSError as ioerr: - _LOGGER.warning("Polling error %s", ioerr) - return - except BluetoothBackendException as bterror: - _LOGGER.warning("Polling error %s", bterror) - return - - if data is not None: - _LOGGER.debug("%s = %s", self.name, data) - self.data.append(data) - else: - _LOGGER.warning( - "Did not receive any data from Mi Temp sensor %s", self.name - ) - # Remove old data from median list or set sensor value to None - # if no data is available anymore - if self.data: - self.data = self.data[1:] - else: - self._attr_native_value = None - return - - if len(self.data) > self.median_count: - self.data = self.data[1:] - - if len(self.data) == self.median_count: - median = sorted(self.data)[int((self.median_count - 1) / 2)] - _LOGGER.debug("Median is: %s", median) - self._attr_native_value = median - else: - _LOGGER.debug("Not yet enough data for median calculation") diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json new file mode 100644 index 00000000000..1f9f031a3bb --- /dev/null +++ b/homeassistant/components/mitemp_bt/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/mitemp_bt/translations/de.json b/homeassistant/components/mitemp_bt/translations/de.json new file mode 100644 index 00000000000..3c9e6960aeb --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t mit der neuen Integration manuell hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors wurde ersetzt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/el.json b/homeassistant/components/mitemp_bt/translations/el.json new file mode 100644 index 00000000000..f8ac6849c10 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 Xiaomi Mijia BLE \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant 2022.7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Xiaomi BLE \u03c3\u03c4\u03b7\u03bd \u03ba\u03c5\u03ba\u03bb\u03bf\u03c6\u03bf\u03c1\u03af\u03b1 \u03c4\u03bf\u03c5 2022.8. \n\n \u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Mijia BLE \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 Xiaomi Mijia BLE \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 Xiaomi Mijia BLE \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/en.json b/homeassistant/components/mitemp_bt/translations/en.json new file mode 100644 index 00000000000..78ec041405b --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/it.json b/homeassistant/components/mitemp_bt/translations/it.json new file mode 100644 index 00000000000..cc383e4184c --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor \u00e8 stata sostituita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pl.json b/homeassistant/components/mitemp_bt/translations/pl.json new file mode 100644 index 00000000000..08770ff011b --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integracja sensora temperatury i wilgotno\u015bci Xiaomi Mijia BLE przesta\u0142a dzia\u0142a\u0107 w Home Assistant 2022.7 i zosta\u0142a zast\u0105piona integracj\u0105 Xiaomi BLE w wersji 2022.8. \n\nNie ma mo\u017cliwo\u015bci migracji, dlatego musisz r\u0119cznie doda\u0107 swoje urz\u0105dzenie Xiaomi Mijia BLE przy u\u017cyciu nowej integracji. \n\nTwoja istniej\u0105ca konfiguracja YAML dla sensora temperatury i wilgotno\u015bci Xiaomi Mijia BLE nie jest ju\u017c u\u017cywana przez Home Assistanta. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja sensora temperatury i wilgotno\u015bci Xiaomi Mijia BLE zosta\u0142a zast\u0105piona" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pt-BR.json b/homeassistant/components/mitemp_bt/translations/pt-BR.json new file mode 100644 index 00000000000..634f5dd71fd --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade do Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE foi substitu\u00edda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/zh-Hant.json b/homeassistant/components/mitemp_bt/translations/zh-Hant.json new file mode 100644 index 00000000000..05799bbd712 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/__init__.py b/homeassistant/components/mjpeg/__init__.py index 632156b7adc..605c8b6c9d5 100644 --- a/homeassistant/components/mjpeg/__init__.py +++ b/homeassistant/components/mjpeg/__init__.py @@ -24,7 +24,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Reload entry when its updated. entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index 91b2271aafa..5c42d392142 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -85,7 +85,7 @@ class MjpegCamera(Camera): def __init__( self, *, - name: str, + name: str | None = None, mjpeg_url: str, still_image_url: str | None, authentication: str | None = None, diff --git a/homeassistant/components/mjpeg/translations/cs.json b/homeassistant/components/mjpeg/translations/cs.json index 00616fbdd50..3fc5acfe8dd 100644 --- a/homeassistant/components/mjpeg/translations/cs.json +++ b/homeassistant/components/mjpeg/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, diff --git a/homeassistant/components/mjpeg/translations/pt.json b/homeassistant/components/mjpeg/translations/pt.json new file mode 100644 index 00000000000..1967a9e4a50 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "password": "Palavra-passe" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py new file mode 100644 index 00000000000..237948a8ff6 --- /dev/null +++ b/homeassistant/components/moat/__init__.py @@ -0,0 +1,42 @@ +"""The Moat Bluetooth BLE integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Moat BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py new file mode 100644 index 00000000000..6f51b62d110 --- /dev/null +++ b/homeassistant/components/moat/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for moat ble integration.""" +from __future__ import annotations + +from typing import Any + +from moat_ble import MoatBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class MoatConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for moat.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/moat/const.py b/homeassistant/components/moat/const.py new file mode 100644 index 00000000000..c2975343758 --- /dev/null +++ b/homeassistant/components/moat/const.py @@ -0,0 +1,3 @@ +"""Constants for the Moat Bluetooth integration.""" + +DOMAIN = "moat" diff --git a/homeassistant/components/moat/manifest.json b/homeassistant/components/moat/manifest.json new file mode 100644 index 00000000000..49e6985d1c1 --- /dev/null +++ b/homeassistant/components/moat/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "moat", + "name": "Moat", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/moat", + "bluetooth": [{ "local_name": "Moat_S*" }], + "requirements": ["moat-ble==0.1.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py new file mode 100644 index 00000000000..295e2877aed --- /dev/null +++ b/homeassistant/components/moat/sensor.py @@ -0,0 +1,164 @@ +"""Support for moat ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from moat_ble import ( + DeviceClass, + DeviceKey, + MoatBluetoothDeviceData, + SensorDeviceInfo, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ELECTRIC_POTENTIAL_VOLT, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( + key=f"{DeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_VOLT}", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to hass device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Moat BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + data = MoatBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload( + processor.async_add_entities_listener( + MoatBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class MoatBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a moat ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/moat/strings.json b/homeassistant/components/moat/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/moat/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "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%]" + } + } +} diff --git a/homeassistant/components/moat/translations/ca.json b/homeassistant/components/moat/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/moat/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/de.json b/homeassistant/components/moat/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/moat/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/el.json b/homeassistant/components/moat/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/moat/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/en.json b/homeassistant/components/moat/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/moat/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/et.json b/homeassistant/components/moat/translations/et.json new file mode 100644 index 00000000000..8dc1b9f6ed0 --- /dev/null +++ b/homeassistant/components/moat/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/fr.json b/homeassistant/components/moat/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/moat/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/hu.json b/homeassistant/components/moat/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/moat/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/id.json b/homeassistant/components/moat/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/moat/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/it.json b/homeassistant/components/moat/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/moat/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/ja.json b/homeassistant/components/moat/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/moat/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/pl.json b/homeassistant/components/moat/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/moat/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/pt-BR.json b/homeassistant/components/moat/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/moat/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/ru.json b/homeassistant/components/moat/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/moat/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/zh-Hant.json b/homeassistant/components/moat/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/moat/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 1b308705624..70c23da66e2 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,5 +1,6 @@ """Integrates Native Apps to Home Assistant.""" from contextlib import suppress +from typing import Any from homeassistant.components import cloud, notify as hass_notify from homeassistant.components.webhook import ( @@ -38,7 +39,7 @@ PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the mobile app component.""" - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) if (app_config := await store.async_load()) is None or not isinstance( app_config, dict ): @@ -96,7 +97,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: registration_name = f"Mobile App: {registration[ATTR_DEVICE_NAME]}" webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass_notify.async_reload(hass, DOMAIN) diff --git a/homeassistant/components/mobile_app/translations/ja.json b/homeassistant/components/mobile_app/translations/ja.json index 1f848fa1988..fa8ae167b1d 100644 --- a/homeassistant/components/mobile_app/translations/ja.json +++ b/homeassistant/components/mobile_app/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Mobile app\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u3044\u3066\u3001Home Assistant\u3068\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u3002\u4e92\u63db\u6027\u306e\u3042\u308b\u30a2\u30d7\u30ea\u306e\u4e00\u89a7\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({apps_url})\u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002" + "install_app": "Mobile app\u7d71\u5408\u3092\u958b\u3044\u3066\u3001Home Assistant\u3068\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u3002\u4e92\u63db\u6027\u306e\u3042\u308b\u30a2\u30d7\u30ea\u306e\u4e00\u89a7\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({apps_url})\u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/modem_callerid/__init__.py b/homeassistant/components/modem_callerid/__init__.py index 8f62cf4beb5..bbdd1b05383 100644 --- a/homeassistant/components/modem_callerid/__init__.py +++ b/homeassistant/components/modem_callerid/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Unable to open port: {device}") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index a5b9df933ef..35ad69b3e1c 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -10,14 +10,14 @@ }, "step": { "usb_confirm": { - "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u7d71\u5408\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" }, "user": { "data": { "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8" }, - "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u7d71\u5408\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/modem_callerid/translations/pt.json b/homeassistant/components/modem_callerid/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index ed4212d9444..9b425f61ad0 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/modern_forms/translations/pt.json b/homeassistant/components/modern_forms/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index 64bdfeb4e6d..b2d3438250f 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/pt.json b/homeassistant/components/moehlenhoff_alpha2/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 91fd353f2e0..1c9a2fa7868 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: FIRST_RUN: first_run, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/monoprice/translations/bg.json b/homeassistant/components/monoprice/translations/bg.json index 40cf11b9651..0dbc6308db2 100644 --- a/homeassistant/components/monoprice/translations/bg.json +++ b/homeassistant/components/monoprice/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/moon/__init__.py b/homeassistant/components/moon/__init__.py index 0b36ba59198..e2eaaf89948 100644 --- a/homeassistant/components/moon/__init__.py +++ b/homeassistant/components/moon/__init__.py @@ -7,7 +7,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index c5078771af8..f3a3b3f48fa 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -12,6 +12,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -72,11 +74,17 @@ class MoonSensorEntity(SensorEntity): """Representation of a Moon sensor.""" _attr_device_class = "moon__phase" + _attr_has_entity_name = True + _attr_name = "Phase" def __init__(self, entry: ConfigEntry) -> None: """Initialize the moon sensor.""" - self._attr_name = entry.title self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + name="Moon", + identifiers={(DOMAIN, entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) async def async_update(self) -> None: """Get the time and updates the states.""" diff --git a/homeassistant/components/moon/translations/ja.json b/homeassistant/components/moon/translations/ja.json index 6544580781f..f7678a63278 100644 --- a/homeassistant/components/moon/translations/ja.json +++ b/homeassistant/components/moon/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 95ac1c5fd44..dfbc6ab74a7 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -65,36 +65,43 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): self._wait_for_push = coordinator_info[CONF_WAIT_FOR_PUSH] def update_gateway(self): - """Call all updates using one async_add_executor_job.""" - data = {} - + """Fetch data from gateway.""" try: self._gateway.Update() except (timeout, ParseException): # let the error be logged and handled by the motionblinds library - data[KEY_GATEWAY] = {ATTR_AVAILABLE: False} - return data + return {ATTR_AVAILABLE: False} else: - data[KEY_GATEWAY] = {ATTR_AVAILABLE: True} + return {ATTR_AVAILABLE: True} - for blind in self._gateway.device_list.values(): - try: - if self._wait_for_push: - blind.Update() - else: - blind.Update_trigger() - except (timeout, ParseException): - # let the error be logged and handled by the motionblinds library - data[blind.mac] = {ATTR_AVAILABLE: False} + def update_blind(self, blind): + """Fetch data from a blind.""" + try: + if self._wait_for_push: + blind.Update() else: - data[blind.mac] = {ATTR_AVAILABLE: True} - - return data + blind.Update_trigger() + except (timeout, ParseException): + # let the error be logged and handled by the motionblinds library + return {ATTR_AVAILABLE: False} + else: + return {ATTR_AVAILABLE: True} async def _async_update_data(self): """Fetch the latest data from the gateway and blinds.""" + data = {} + async with self.api_lock: - data = await self.hass.async_add_executor_job(self.update_gateway) + data[KEY_GATEWAY] = await self.hass.async_add_executor_job( + self.update_gateway + ) + + for blind in self._gateway.device_list.values(): + await asyncio.sleep(1.5) + async with self.api_lock: + data[blind.mac] = await self.hass.async_add_executor_job( + self.update_blind, blind + ) all_available = all(device[ATTR_AVAILABLE] for device in data.values()) if all_available: @@ -204,7 +211,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=version, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index bc09d3e9e38..3499932c1d8 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.8"], + "requirements": ["motionblinds==0.6.11"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/homeassistant/components/motion_blinds/translations/pt.json b/homeassistant/components/motion_blinds/translations/pt.json index 7538043f1ce..ccf03b80e43 100644 --- a/homeassistant/components/motion_blinds/translations/pt.json +++ b/homeassistant/components/motion_blinds/translations/pt.json @@ -5,7 +5,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "connection_error": "Falha na liga\u00e7\u00e3o" }, - "flow_title": "Cortinas Motion", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 279cc8bde70..7c87dda1bd2 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -1,7 +1,6 @@ """The motionEye integration.""" from __future__ import annotations -import asyncio from collections.abc import Callable import contextlib from http import HTTPStatus @@ -329,7 +328,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "motionEye", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - @callback async def async_update_data() -> dict[str, Any] | None: try: return await client.async_get_cameras() @@ -392,20 +390,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: device_registry.async_remove_device(device_entry.id) - async def setup_then_listen() -> None: - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - entry.async_on_unload( - coordinator.async_add_listener(_async_process_motioneye_cameras) - ) - await coordinator.async_refresh() - entry.async_on_unload(entry.add_update_listener(_async_entry_updated)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload( + coordinator.async_add_listener(_async_process_motioneye_cameras) + ) + await coordinator.async_refresh() + entry.async_on_unload(entry.add_update_listener(_async_entry_updated)) - hass.async_create_task(setup_then_listen()) return True diff --git a/homeassistant/components/motioneye/translations/bg.json b/homeassistant/components/motioneye/translations/bg.json index ef56ca3f2df..d2db5257b51 100644 --- a/homeassistant/components/motioneye/translations/bg.json +++ b/homeassistant/components/motioneye/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d URL" }, "step": { "user": { diff --git a/homeassistant/components/motioneye/translations/pt.json b/homeassistant/components/motioneye/translations/pt.json new file mode 100644 index 00000000000..848cfe1ac2d --- /dev/null +++ b/homeassistant/components/motioneye/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a099e7b580c..2bea1a593d1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -3,15 +3,13 @@ from __future__ import annotations import asyncio from collections.abc import Callable -from dataclasses import dataclass -import datetime as dt import logging from typing import Any, cast import jinja2 import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config as conf_util, config_entries from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -20,11 +18,9 @@ from homeassistant.const import ( CONF_PAYLOAD, CONF_PORT, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) -from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback -from homeassistant.data_entry_flow import BaseServiceInfo +from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry @@ -65,9 +61,10 @@ from .const import ( # noqa: F401 CONF_TOPIC, CONF_WILL_MESSAGE, CONFIG_ENTRY_IS_SETUP, - DATA_CONFIG_ENTRY_LOCK, DATA_MQTT, DATA_MQTT_CONFIG, + DATA_MQTT_RELOAD_DISPATCHERS, + DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_RELOAD_NEEDED, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, @@ -87,7 +84,12 @@ from .models import ( # noqa: F401 ReceiveMessage, ReceivePayloadType, ) -from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic +from .util import ( + _VALID_QOS_SCHEMA, + mqtt_config_entry_enabled, + valid_publish_topic, + valid_subscribe_topic, +) _LOGGER = logging.getLogger(__name__) @@ -105,6 +107,16 @@ CONNECTION_SUCCESS = "connection_success" CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" +CONFIG_ENTRY_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -140,18 +152,6 @@ MQTT_PUBLISH_SCHEMA = vol.All( ) -@dataclass -class MqttServiceInfo(BaseServiceInfo): - """Prepared info from mqtt entries.""" - - topic: str - payload: ReceivePayloadType - qos: int - retain: bool - subscribed_topic: str - timestamp: dt.datetime - - async def _async_setup_discovery( hass: HomeAssistant, conf: ConfigType, config_entry ) -> None: @@ -174,7 +174,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf = dict(conf) hass.data[DATA_MQTT_CONFIG] = conf - if not bool(hass.config_entries.async_entries(DOMAIN)): + if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is None: # Create an import flow if the user has yaml configured entities etc. # but no broker configuration. Note: The intention is not for this to # import broker configuration from YAML because that has been deprecated. @@ -185,9 +185,33 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: data={}, ) ) + hass.data[DATA_MQTT_RELOAD_NEEDED] = True + elif mqtt_entry_status is False: + _LOGGER.info( + "MQTT will be not available until the config entry is enabled", + ) + hass.data[DATA_MQTT_RELOAD_NEEDED] = True + return True +def _filter_entry_config(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove unknown keys from config entry data. + + Extra keys may have been added when importing MQTT yaml configuration. + """ + filtered_data = { + k: entry.data[k] for k in CONFIG_ENTRY_CONFIG_KEYS if k in entry.data + } + if entry.data.keys() != filtered_data.keys(): + _LOGGER.warning( + "The following unsupported configuration options were removed from the " + "MQTT config entry: %s. Add them to configuration.yaml if they are needed", + entry.data.keys() - filtered_data.keys(), + ) + hass.config_entries.async_update_entry(entry, data=filtered_data) + + def _merge_basic_config( hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any] ) -> None: @@ -239,17 +263,22 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: - """Load a config entry.""" +async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None: + """Fetch fresh MQTT yaml config from the hass config when (re)loading the entry.""" + if DATA_MQTT_RELOAD_ENTRY in hass.data: + hass_config = await conf_util.async_hass_config_yaml(hass) + mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) + hass.data[DATA_MQTT_CONFIG] = mqtt_config + + # Remove unknown keys from config entry data + _filter_entry_config(hass, entry) + # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) - # Bail out if broker setting is missing if CONF_BROKER not in entry.data: _LOGGER.error("MQTT broker is not configured, please configure it") - return False + return None # If user doesn't have configuration.yaml config, generate default values # for options not in config entry data @@ -271,22 +300,21 @@ async def async_setup_entry( # noqa: C901 # Merge advanced configuration values from configuration.yaml conf = _merge_extended_config(entry, conf) + return conf - hass.data[DATA_MQTT] = MQTT( - hass, - entry, - conf, - ) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Load a config entry.""" + # Merge basic configuration, and add missing defaults for basic options + if (conf := await async_fetch_config(hass, entry)) is None: + # Bail out + return False + + hass.data[DATA_MQTT] = MQTT(hass, entry, conf) entry.add_update_listener(_async_config_entry_updated) await hass.data[DATA_MQTT].async_connect() - async def async_stop_mqtt(_event: Event): - """Stop MQTT component.""" - await hass.data[DATA_MQTT].async_disconnect() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) - async def async_publish_service(call: ServiceCall) -> None: """Handle MQTT publish service calls.""" msg_topic = call.data.get(ATTR_TOPIC) @@ -375,7 +403,6 @@ async def async_setup_entry( # noqa: C901 ) # setup platforms and discovery - hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() async def async_setup_reload_service() -> None: @@ -411,6 +438,7 @@ async def async_setup_entry( # noqa: C901 # pylint: disable-next=import-outside-toplevel from . import device_automation, tag + # Forward the entry setup to the MQTT platforms await asyncio.gather( *( [ @@ -428,21 +456,25 @@ async def async_setup_entry( # noqa: C901 await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded await async_setup_reload_service() - if DATA_MQTT_RELOAD_NEEDED in hass.data: hass.data.pop(DATA_MQTT_RELOAD_NEEDED) - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=False, - ) + await async_reload_manual_mqtt_items(hass) - hass.async_create_task(async_forward_entry_setup_and_setup_discovery(entry)) + await async_forward_entry_setup_and_setup_discovery(entry) return True +async def async_reload_manual_mqtt_items(hass: HomeAssistant) -> None: + """Reload manual configured MQTT items.""" + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + + @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} ) @@ -544,3 +576,49 @@ async def async_remove_config_entry_device( await device_automation.async_removed_from_device(hass, device_entry.id) return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload MQTT dump and publish service when the config entry is unloaded.""" + # Unload publish and dump services. + hass.services.async_remove( + DOMAIN, + SERVICE_PUBLISH, + ) + hass.services.async_remove( + DOMAIN, + SERVICE_DUMP, + ) + + # Stop the discovery + await discovery.async_stop(hass) + mqtt_client: MQTT = hass.data[DATA_MQTT] + # Unload the platforms + await asyncio.gather( + *( + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ) + ) + await hass.async_block_till_done() + # Unsubscribe reload dispatchers + while reload_dispatchers := hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []): + reload_dispatchers.pop()() + hass.data[CONFIG_ENTRY_IS_SETUP] = set() + # Cleanup listeners + mqtt_client.cleanup() + + # Trigger reload manual MQTT items at entry setup + # Reload the legacy yaml platform + await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False: + # The entry is disabled reload legacy manual items when the entry is enabled again + hass.data[DATA_MQTT_RELOAD_NEEDED] = True + elif mqtt_entry_status is True: + # The entry is reloaded: + # Trigger re-fetching the yaml config at entry setup + hass.data[DATA_MQTT_RELOAD_ENTRY] = True + # Stop the loop + await mqtt_client.async_disconnect() + + return True diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index b6f2f8f236e..cf7262f9468 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -156,8 +156,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Alarm Control Panel platform.""" async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 9e0a049b15e..cffb2fd8300 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -111,8 +112,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT binary sensor.""" async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) @@ -291,12 +296,12 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): return self._state @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this sensor.""" return self._config.get(CONF_DEVICE_CLASS) @property - def force_update(self): + def force_update(self) -> bool: """Force update.""" return self._config[CONF_FORCE_UPDATE] diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index b75fbe4b97f..0881b963b04 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -91,8 +91,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT button.""" async_add_entities([MqttButton(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 69af7992229..f213bec9bb6 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -89,8 +89,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Camera.""" async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index d676c128260..192de624f17 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Iterable +from collections.abc import Awaitable, Callable, Coroutine, Iterable from functools import lru_cache, partial, wraps import inspect from itertools import groupby @@ -15,6 +15,7 @@ import uuid import attr import certifi +from paho.mqtt.client import MQTTMessage from homeassistant.const import ( CONF_CLIENT_ID, @@ -23,8 +24,9 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, HassJob, HomeAssistant, callback +from homeassistant.core import CoreState, Event, HassJob, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -59,6 +61,7 @@ from .models import ( ReceiveMessage, ReceivePayloadType, ) +from .util import mqtt_config_entry_enabled if TYPE_CHECKING: # Only import for paho-mqtt type checking here, imports are done locally @@ -95,6 +98,10 @@ async def async_publish( ) -> None: """Publish message to a MQTT topic.""" + if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + raise HomeAssistantError( + f"Cannot publish to topic '{topic}', MQTT is not enabled" + ) outgoing_payload = payload if not isinstance(payload, bytes): if not encoding: @@ -174,6 +181,10 @@ async def async_subscribe( Call the return value to unsubscribe. """ + if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + raise HomeAssistantError( + f"Cannot subscribe to topic '{topic}', MQTT is not enabled" + ) # Count callback parameters which don't have a default value non_default = 0 if msg_callback: @@ -236,7 +247,7 @@ class Subscription: topic: str = attr.ib() matcher: Any = attr.ib() - job: HassJob = attr.ib() + job: HassJob[[ReceiveMessage], Coroutine[Any, Any, None] | None] = attr.ib() qos: int = attr.ib(default=0) encoding: str | None = attr.ib(default="utf-8") @@ -315,9 +326,11 @@ class MQTT: self._ha_started = asyncio.Event() self._last_subscribe = time.time() self._mqttc: mqtt.Client = None - self._paho_lock = asyncio.Lock() + self._cleanup_on_unload: list[Callable] = [] - self._pending_operations: dict[str, asyncio.Event] = {} + self._paho_lock = asyncio.Lock() # Prevents parallel calls to the MQTT client + self._pending_operations: dict[int, asyncio.Event] = {} + self._pending_operations_condition = asyncio.Condition() if self.hass.state == CoreState.running: self._ha_started.set() @@ -331,6 +344,19 @@ class MQTT: self.init_client() + async def async_stop_mqtt(_event: Event): + """Stop MQTT component.""" + await self.async_disconnect() + + self._cleanup_on_unload.append( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) + ) + + def cleanup(self): + """Clean up listeners.""" + while self._cleanup_on_unload: + self._cleanup_on_unload.pop()() + def init_client(self): """Initialize paho client.""" self._mqttc = MqttClientSetup(self.conf).client @@ -405,12 +431,21 @@ class MQTT: # Do not disconnect, we want the broker to always publish will self._mqttc.loop_stop() + def no_more_acks() -> bool: + """Return False if there are unprocessed ACKs.""" + return not bool(self._pending_operations) + + # wait for ACK-s to be processesed (unsubscribe only) + async with self._pending_operations_condition: + await self._pending_operations_condition.wait_for(no_more_acks) + + # stop the MQTT loop await self.hass.async_add_executor_job(stop) async def async_subscribe( self, topic: str, - msg_callback: MessageCallbackType, + msg_callback: AsyncMessageCallbackType | MessageCallbackType, qos: int, encoding: str | None = None, ) -> Callable[[], None]: @@ -440,7 +475,7 @@ class MQTT: self.subscriptions.remove(subscription) self._matching_subscriptions.cache_clear() - # Only unsubscribe if currently connected. + # Only unsubscribe if currently connected if self.connected: self.hass.async_create_task(self._async_unsubscribe(topic)) @@ -451,18 +486,22 @@ class MQTT: This method is a coroutine. """ + + def _client_unsubscribe(topic: str) -> int: + result: int | None = None + result, mid = self._mqttc.unsubscribe(topic) + _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) + _raise_on_error(result) + return mid + if any(other.topic == topic for other in self.subscriptions): # Other subscriptions on topic remaining - don't unsubscribe. return async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.unsubscribe, topic - ) - _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) + mid = await self.hass.async_add_executor_job(_client_unsubscribe, topic) + await self._register_mid(mid) + self.hass.async_create_task(self._wait_for_mid(mid)) async def _async_perform_subscriptions( self, subscriptions: Iterable[tuple[str, int]] @@ -559,15 +598,15 @@ class MQTT: self.hass.add_job(self._mqtt_handle_message, msg) @lru_cache(2048) - def _matching_subscriptions(self, topic): - subscriptions = [] + def _matching_subscriptions(self, topic: str) -> list[Subscription]: + subscriptions: list[Subscription] = [] for subscription in self.subscriptions: if subscription.matcher(topic): subscriptions.append(subscription) return subscriptions @callback - def _mqtt_handle_message(self, msg) -> None: + def _mqtt_handle_message(self, msg: MQTTMessage) -> None: _LOGGER.debug( "Received message on %s%s: %s", msg.topic, @@ -610,14 +649,18 @@ class MQTT: """Publish / Subscribe / Unsubscribe callback.""" self.hass.add_job(self._mqtt_handle_mid, mid) - @callback - def _mqtt_handle_mid(self, mid) -> None: + async def _mqtt_handle_mid(self, mid: int) -> None: # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() + await self._register_mid(mid) self._pending_operations[mid].set() + async def _register_mid(self, mid: int) -> None: + """Create Event for an expected ACK.""" + async with self._pending_operations_condition: + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: """Disconnected callback.""" self.connected = False @@ -629,12 +672,11 @@ class MQTT: result_code, ) - async def _wait_for_mid(self, mid): + async def _wait_for_mid(self, mid: int) -> None: """Wait for ACK from broker.""" # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() + await self._register_mid(mid) try: await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) except asyncio.TimeoutError: @@ -642,7 +684,10 @@ class MQTT: "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid ) finally: - del self._pending_operations[mid] + async with self._pending_operations_condition: + # Cleanup ACK sync buffer + del self._pending_operations[mid] + self._pending_operations_condition.notify_all() async def _discovery_cooldown(self): now = time.time() diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 6b09891483c..30263798740 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations import functools import logging +from typing import Any import voluptuous as vol @@ -400,8 +401,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT climate devices.""" async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)]) @@ -754,29 +759,29 @@ class MqttClimate(MqttEntity, ClimateEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self._config.get(CONF_TEMPERATURE_UNIT): - return self._config.get(CONF_TEMPERATURE_UNIT) + if unit := self._config.get(CONF_TEMPERATURE_UNIT): + return unit return self.hass.config.units.temperature_unit @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._current_temp @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._target_temp @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the low target temperature we try to reach.""" return self._target_temp_low @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the high target temperature we try to reach.""" return self._target_temp_high @@ -796,7 +801,7 @@ class MqttClimate(MqttEntity, ClimateEntity): return self._config[CONF_MODE_LIST] @property - def target_temperature_step(self): + def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" return self._config[CONF_TEMP_STEP] @@ -813,7 +818,7 @@ class MqttClimate(MqttEntity, ClimateEntity): return PRESET_NONE @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Return preset modes.""" presets = [] presets.extend(self._preset_modes) @@ -834,17 +839,17 @@ class MqttClimate(MqttEntity, ClimateEntity): return presets @property - def is_aux_heat(self): + def is_aux_heat(self) -> bool | None: """Return true if away mode is on.""" return self._aux @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the fan setting.""" return self._current_fan_mode @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return self._config[CONF_FAN_MODE_LIST] @@ -871,10 +876,9 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[cmnd_template](temp) await self._publish(cmnd_topic, payload) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" - if kwargs.get(ATTR_HVAC_MODE) is not None: - operation_mode = kwargs.get(ATTR_HVAC_MODE) + if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: await self.async_set_hvac_mode(operation_mode) await self._set_temperature( @@ -904,7 +908,7 @@ class MqttClimate(MqttEntity, ClimateEntity): # Always optimistic? self.async_write_ha_state() - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 if self._send_if_off or self._current_operation != HVACMode.OFF: @@ -917,7 +921,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._current_swing_mode = swing_mode self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target temperature.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 if self._send_if_off or self._current_operation != HVACMode.OFF: @@ -928,7 +932,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._current_fan_mode = fan_mode self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" if hvac_mode == HVACMode.OFF: await self._publish( @@ -945,12 +949,12 @@ class MqttClimate(MqttEntity, ClimateEntity): self.async_write_ha_state() @property - def swing_mode(self): + def swing_mode(self) -> str | None: """Return the swing setting.""" return self._current_swing_mode @property - def swing_modes(self): + def swing_modes(self) -> list[str]: """List of available swing modes.""" return self._config[CONF_SWING_MODE_LIST] @@ -1027,16 +1031,16 @@ class MqttClimate(MqttEntity, ClimateEntity): self._aux = state self.async_write_ha_state() - async def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" await self._set_aux_heat(True) - async def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" await self._set_aux_heat(False) @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" support = 0 @@ -1083,18 +1087,18 @@ class MqttClimate(MqttEntity, ClimateEntity): return support @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" return self._config[CONF_TEMP_MIN] @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self._config[CONF_TEMP_MAX] @property - def precision(self): + def precision(self) -> float: """Return the precision of the system.""" - if self._config.get(CONF_PRECISION) is not None: - return self._config.get(CONF_PRECISION) + if (precision := self._config.get(CONF_PRECISION)) is not None: + return precision return super().precision diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a8d0957921d..538c12d258c 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict import queue +from typing import Any import voluptuous as vol @@ -55,14 +56,18 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return MQTTOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return await self.async_step_broker() - async def async_step_broker(self, user_input=None): + async def async_step_broker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm the setup.""" errors = {} @@ -102,9 +107,12 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_hassio_confirm() - async def async_step_hassio_confirm(self, user_input=None): + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm a Hass.io discovery.""" errors = {} + assert self._hassio_discovery if user_input is not None: data = self._hassio_discovery @@ -148,11 +156,13 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self.broker_config: dict[str, str | int] = {} self.options = dict(config_entry.options) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: None = None) -> FlowResult: """Manage the MQTT options.""" return await self.async_step_broker() - async def async_step_broker(self, user_input=None): + async def async_step_broker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the MQTT broker configuration.""" errors = {} current_config = self.config_entry.data @@ -200,12 +210,14 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): last_step=False, ) - async def async_step_options(self, user_input=None): + async def async_step_options( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the MQTT options.""" errors = {} current_config = self.config_entry.data yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) - options_config = {} + options_config: dict[str, Any] = {} if user_input is not None: bad_birth = False bad_will = False @@ -251,7 +263,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self.hass.config_entries.async_update_entry( self.config_entry, data=updated_config ) - return self.async_create_entry(title="", data=None) + return self.async_create_entry(title="", data={}) birth = { **DEFAULT_BIRTH, @@ -269,7 +281,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY) ) - fields = OrderedDict() + fields: OrderedDict[vol.Marker, Any] = OrderedDict() fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool # Birth message is disabled if CONF_BIRTH_MESSAGE = {} diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6ac77021337..6a5cb912fce 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -31,9 +31,11 @@ CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" -DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_MQTT = "mqtt" DATA_MQTT_CONFIG = "mqtt_config" +MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" +DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers" +DATA_MQTT_RELOAD_ENTRY = "mqtt_reload_entry" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 14746329250..b0fbacd10fc 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -251,8 +251,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Cover.""" async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index 1b6c2b25ff3..342817e38cc 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -2,15 +2,43 @@ import voluptuous as vol from homeassistant.components import device_tracker +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from ..const import MQTT_DATA_DEVICE_TRACKER_LEGACY from ..mixins import warn_for_legacy_schema from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401 from .schema_discovery import async_setup_entry_from_discovery -from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml +from .schema_yaml import ( + PLATFORM_SCHEMA_YAML, + MQTTLegacyDeviceTrackerData, + async_setup_scanner_from_yaml, +) # Configuring MQTT Device Trackers under the device_tracker platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA_YAML, warn_for_legacy_schema(device_tracker.DOMAIN) ) + +# Legacy setup async_setup_scanner = async_setup_scanner_from_yaml -async_setup_entry = async_setup_entry_from_discovery + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT device_tracker through configuration.yaml and dynamically through MQTT discovery.""" + await async_setup_entry_from_discovery(hass, config_entry, async_add_entities) + # (re)load legacy service + if MQTT_DATA_DEVICE_TRACKER_LEGACY in hass.data: + yaml_device_tracker_data: MQTTLegacyDeviceTrackerData = hass.data[ + MQTT_DATA_DEVICE_TRACKER_LEGACY + ] + await async_setup_scanner_from_yaml( + hass, + config=yaml_device_tracker_data.config, + async_see=yaml_device_tracker_data.async_see, + ) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 1ba540c8243..105442b176e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,4 +1,6 @@ -"""Support for tracking MQTT enabled devices.""" +"""Support for tracking MQTT enabled devices identified through discovery.""" +from __future__ import annotations + import asyncio import functools @@ -7,6 +9,7 @@ import voluptuous as vol from homeassistant.components import device_tracker from homeassistant.components.device_tracker import SOURCE_TYPES from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, @@ -16,8 +19,10 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType from .. import subscription from ..config import MQTT_RO_SCHEMA @@ -47,7 +52,11 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): +async def async_setup_entry_from_discovery( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml await asyncio.gather( @@ -66,8 +75,12 @@ async def async_setup_entry_from_discovery(hass, config_entry, async_add_entitie async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Device Tracker entity.""" async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py index 2dfa5b7134c..c005a82dbeb 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -1,16 +1,26 @@ """Support for tracking MQTT enabled devices defined in YAML.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +import dataclasses +import logging +from typing import Any import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from ... import mqtt from ..client import async_subscribe from ..config import SCHEMA_BASE -from ..const import CONF_QOS -from ..util import valid_subscribe_topic +from ..const import CONF_QOS, MQTT_DATA_DEVICE_TRACKER_LEGACY +from ..util import mqtt_config_entry_enabled, valid_subscribe_topic + +_LOGGER = logging.getLogger(__name__) CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" @@ -26,13 +36,44 @@ PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend( ) -async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info=None): +@dataclasses.dataclass +class MQTTLegacyDeviceTrackerData: + """Class to hold device tracker data.""" + + async_see: Callable[..., Awaitable[None]] + config: ConfigType + + +async def async_setup_scanner_from_yaml( + hass: HomeAssistant, + config: ConfigType, + async_see: Callable[..., Awaitable[None]], + discovery_info: DiscoveryInfoType | None = None, +) -> bool: """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] payload_home = config[CONF_PAYLOAD_HOME] payload_not_home = config[CONF_PAYLOAD_NOT_HOME] source_type = config.get(CONF_SOURCE_TYPE) + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + subscriptions: list[Callable] = [] + + hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] = MQTTLegacyDeviceTrackerData( + async_see, config + ) + if not mqtt_config_entry_enabled(hass): + _LOGGER.info( + "MQTT device trackers will be not available until the config entry is enabled", + ) + return False + + @callback + def _entry_unload(*_: Any) -> None: + """Handle the unload of the config entry.""" + # Unsubscribe from mqtt + for unsubscribe in subscriptions: + unsubscribe() for dev_id, topic in devices.items(): @@ -52,6 +93,10 @@ async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info= hass.async_create_task(async_see(**see_args)) - await async_subscribe(hass, topic, async_message_received, qos) + subscriptions.append( + await async_subscribe(hass, topic, async_message_received, qos) + ) + + config_entry.async_on_unload(_entry_unload) return True diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 5b39e8fa1b5..ebc3a170aeb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -17,6 +17,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.json import json_loads +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -234,8 +235,7 @@ async def async_start( # noqa: C901 hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None ) - hass.data[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock() - + hass.data.setdefault(DATA_CONFIG_FLOW_LOCK, asyncio.Lock()) hass.data[ALREADY_DISCOVERED] = {} hass.data[PENDING_DISCOVERED] = {} @@ -268,7 +268,7 @@ async def async_start( # noqa: C901 if key not in hass.data[INTEGRATION_UNSUBSCRIBE]: return - data = mqtt.MqttServiceInfo( + data = MqttServiceInfo( topic=msg.topic, payload=msg.payload, qos=msg.qos, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 15e4a80f3e7..20c4936ab38 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -241,8 +241,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT fan.""" async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 5f09fc0d513..3a1271ea2c9 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -3,6 +3,7 @@ from __future__ import annotations import functools import logging +from typing import Any import voluptuous as vol @@ -196,8 +197,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT humidifier.""" async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)]) @@ -407,7 +412,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @@ -431,10 +436,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): """Return the current mode.""" return self._mode - async def async_turn_on( - self, - **kwargs, - ) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the entity. This method is a coroutine. @@ -451,7 +453,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the entity. This method is a coroutine. diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index c7f3395ba4e..76c2980e63b 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,6 +1,7 @@ """Support for MQTT lights.""" from __future__ import annotations +from collections.abc import Callable import functools import voluptuous as vol @@ -120,10 +121,14 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up a MQTT Light.""" - setup_entity = { + setup_entity: dict[str, Callable] = { "basic": async_setup_entity_basic, "json": async_setup_entity_json, "template": async_setup_entity_template, diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index b4788f1db0c..4910eafae75 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -112,8 +112,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Lock platform.""" async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8e59d09dfce..f0cc0c4ad44 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -3,7 +3,8 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine +from functools import partial import logging from typing import Any, Protocol, cast, final @@ -61,7 +62,7 @@ from .const import ( CONF_TOPIC, DATA_MQTT, DATA_MQTT_CONFIG, - DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_RELOAD_DISPATCHERS, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, @@ -84,7 +85,7 @@ from .subscription import ( async_subscribe_topics, async_unsubscribe_topics, ) -from .util import valid_subscribe_topic +from .util import mqtt_config_entry_enabled, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -299,11 +300,24 @@ async def async_get_platform_config_from_yaml( return platform_configs -async def async_setup_entry_helper(hass, domain, async_setup, schema): +async def async_setup_entry_helper( + hass: HomeAssistant, + domain: str, + async_setup: partial[Coroutine[HomeAssistant, str, None]], + schema: vol.Schema, +) -> None: """Set up entity, automation or tag creation dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add an MQTT entity, automation or tag.""" + if not mqtt_config_entry_enabled(hass): + _LOGGER.warning( + "MQTT integration is disabled, skipping setup of discovered item " + "MQTT %s, payload %s", + domain, + discovery_payload, + ) + return discovery_data = discovery_payload.discovery_data try: config = schema(discovery_payload) @@ -316,8 +330,10 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): ) raise - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover + hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []).append( + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover + ) ) @@ -328,16 +344,17 @@ async def async_setup_platform_helper( async_add_entities: AddEntitiesCallback, async_setup_entities: SetupEntity, ) -> None: - """Return true if platform setup should be aborted.""" - if not bool(hass.config_entries.async_entries(DOMAIN)): - hass.data[DATA_MQTT_RELOAD_NEEDED] = None + """Help to set up the platform for manual configured MQTT entities.""" + if not (entry_status := mqtt_config_entry_enabled(hass)): _LOGGER.warning( - "MQTT integration is not setup, skipping setup of manually configured " - "MQTT %s", + "MQTT integration is %s, skipping setup of manually configured MQTT %s", + "not setup" if entry_status is None else "disabled", platform_domain, ) return - await async_setup_entities(hass, async_add_entities, config) + # Ensure we set config_entry when entries are set up to enable clean up + config_entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + await async_setup_entities(hass, async_add_entities, config, config_entry) def init_entity_id_from_config(hass, entity, config, entity_id_format): @@ -355,7 +372,7 @@ class MqttAttributes(Entity): def __init__(self, config: dict) -> None: """Initialize the JSON attributes mixin.""" - self._attributes: dict | None = None + self._attributes: dict[str, Any] | None = None self._attributes_sub_state = None self._attributes_config = config @@ -426,7 +443,7 @@ class MqttAttributes(Entity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self._attributes @@ -640,6 +657,7 @@ class MqttDiscoveryDeviceUpdate: MQTT_DISCOVERY_UPDATED.format(discovery_hash), self.async_discovery_update, ) + config_entry.async_on_unload(self._entry_unload) if device_id is not None: self._remove_device_updated = hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self._async_device_removed @@ -650,6 +668,14 @@ class MqttDiscoveryDeviceUpdate: discovery_hash, ) + @callback + def _entry_unload(self, *_: Any) -> None: + """Handle cleanup when the config entry is unloaded.""" + stop_discovery_updates( + self.hass, self._discovery_data, self._remove_discovery_updated + ) + self._config_entry.async_create_task(self.hass, self.async_tear_down()) + async def async_discovery_update( self, discovery_payload: DiscoveryInfoType | None, @@ -734,7 +760,11 @@ class MqttDiscoveryDeviceUpdate: class MqttDiscoveryUpdate(Entity): """Mixin used to handle updated discovery message for entity based platforms.""" - def __init__(self, discovery_data, discovery_update=None) -> None: + def __init__( + self, + discovery_data: dict, + discovery_update: Callable | None = None, + ) -> None: """Initialize the discovery update mixin.""" self._discovery_data = discovery_data self._discovery_update = discovery_update diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 9bce6baab8b..84bf704a262 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -2,7 +2,7 @@ from __future__ import annotations from ast import literal_eval -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine import datetime as dt from typing import Any, Union @@ -12,12 +12,12 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import template from homeassistant.helpers.entity import Entity +from homeassistant.helpers.service_info.mqtt import ReceivePayloadType from homeassistant.helpers.typing import TemplateVarsType _SENTINEL = object() PublishPayloadType = Union[str, bytes, int, float, None] -ReceivePayloadType = Union[str, bytes] @attr.s(slots=True, frozen=True) @@ -42,7 +42,7 @@ class ReceiveMessage: timestamp: dt.datetime = attr.ib(default=None) -AsyncMessageCallbackType = Callable[[ReceiveMessage], Awaitable[None]] +AsyncMessageCallbackType = Callable[[ReceiveMessage], Coroutine[Any, Any, None]] MessageCallbackType = Callable[[ReceiveMessage], None] diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index dc27a740720..fbadd653df7 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -12,6 +12,7 @@ from homeassistant.components.number import ( DEFAULT_MIN_VALUE, DEFAULT_STEP, DEVICE_CLASSES_SCHEMA, + NumberDeviceClass, RestoreNumber, ) from homeassistant.config_entries import ConfigEntry @@ -144,8 +145,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT number.""" async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)]) @@ -292,11 +297,11 @@ class MqttNumber(MqttEntity, RestoreNumber): ) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def device_class(self) -> str | None: + def device_class(self) -> NumberDeviceClass | None: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 8b654f7cca0..62de54505eb 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -88,8 +88,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT scene.""" async_add_entities([MqttScene(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 4c302446b19..ec88b1732d4 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -103,8 +103,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT select.""" async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)]) @@ -211,6 +215,6 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): ) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 6948e173039..4c04d6176f1 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -156,8 +156,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config: ConfigType, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up MQTT sensor.""" async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) @@ -347,7 +351,7 @@ class MqttSensor(MqttEntity, RestoreSensor): return self._config.get(CONF_UNIT_OF_MEASUREMENT) @property - def force_update(self): + def force_update(self) -> bool: """Force update.""" return self._config[CONF_FORCE_UPDATE] diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index dfb89d2ee79..5ed76fd6330 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -152,8 +152,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT siren.""" async_add_entities([MqttSiren(hass, config, config_entry, discovery_data)]) @@ -309,12 +313,12 @@ class MqttSiren(MqttEntity, SirenEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" mqtt_attributes = super().extra_state_attributes attributes = ( @@ -353,7 +357,7 @@ class MqttSiren(MqttEntity, SirenEntity): self._config[CONF_ENCODING], ) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the siren on. This method is a coroutine. @@ -371,7 +375,7 @@ class MqttSiren(MqttEntity, SirenEntity): self._update(kwargs) self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the siren off. This method is a coroutine. diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index b04f2433659..b5c7ab13dfc 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -2,11 +2,16 @@ from __future__ import annotations import functools +from typing import Any import voluptuous as vol from homeassistant.components import switch -from homeassistant.components.switch import DEVICE_CLASSES_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + DEVICE_CLASSES_SCHEMA, + SwitchDeviceClass, + SwitchEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -106,8 +111,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT switch.""" async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) @@ -195,16 +204,16 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): return self._state @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def device_class(self) -> str | None: + def device_class(self) -> SwitchDeviceClass | None: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on. This method is a coroutine. @@ -221,7 +230,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 9452d5fc259..dc4cc0e109d 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_PLATFORM, CONF_VALUE_TEMPLATE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -128,7 +128,6 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): async def subscribe_topics(self) -> None: """Subscribe to MQTT topics.""" - @callback async def tag_scanned(msg: ReceiveMessage) -> None: tag_id = self._value_template(msg.payload, "").strip() if not tag_id: # No output from template, ignore diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index f69eb1b4cc9..93b1d77dcfa 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -27,6 +27,13 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" + } + }, "options": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 59dff554676..07af6230aa6 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/mqtt/translations/pt.json b/homeassistant/components/mqtt/translations/pt.json index 209c33cf165..6ff10cf515c 100644 --- a/homeassistant/components/mqtt/translations/pt.json +++ b/homeassistant/components/mqtt/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do MQTT \u00e9 permitida." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel ligar ao broker." @@ -57,7 +57,7 @@ "will_retain": "Reter mensagem testamental", "will_topic": "T\u00f3pico da mensagem testamental" }, - "description": "Por favor, selecione as op\u00e7\u00f5es do MQTT." + "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect." } } } diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 66eec1bdfe8..9ef30da7f3b 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -1,9 +1,13 @@ """Utility functions for the MQTT integration.""" + +from __future__ import annotations + from typing import Any import voluptuous as vol from homeassistant.const import CONF_PAYLOAD +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, template from .const import ( @@ -13,9 +17,17 @@ from .const import ( ATTR_TOPIC, DEFAULT_QOS, DEFAULT_RETAIN, + DOMAIN, ) +def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None: + """Return true when the MQTT config entry is enabled.""" + if not bool(hass.config_entries.async_entries(DOMAIN)): + return None + return not bool(hass.config_entries.async_entries(DOMAIN)[0].disabled_by) + + def valid_topic(value: Any) -> str: """Validate that this is a valid topic name/filter.""" value = cv.string(value) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index c49b8cfa012..cdd14e6d8e3 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -100,10 +100,17 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT vacuum.""" - setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} + setup_entity = { + LEGACY: async_setup_entity_legacy, + STATE: async_setup_entity_state, + } await setup_entity[config[CONF_SCHEMA]]( hass, config, async_add_entities, config_entry, discovery_data ) diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 5d558ee0ade..7934220cf91 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mullvad/translations/ja.json b/homeassistant/components/mullvad/translations/ja.json index fc1791527f9..3119597977a 100644 --- a/homeassistant/components/mullvad/translations/ja.json +++ b/homeassistant/components/mullvad/translations/ja.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Mullvad VPN\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + "description": "Mullvad VPN\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } } diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py index 4cdd97f446b..aa5e0d70fe9 100644 --- a/homeassistant/components/mutesync/__init__.py +++ b/homeassistant/components/mutesync/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mutesync/translations/de.json b/homeassistant/components/mutesync/translations/de.json index dccab9e8d1e..9ff12a5fad5 100644 --- a/homeassistant/components/mutesync/translations/de.json +++ b/homeassistant/components/mutesync/translations/de.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Aktiviere die Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", + "invalid_auth": "Aktiviere die Authentifizierung in den Einstellungen von m\u00fctesync \u2192 Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 8bdf07dad75..d5b4730c2de 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 3313a70808c..cc0d918ec05 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -1,7 +1,6 @@ """Connect to a MySensors gateway via pymysensors API.""" from __future__ import annotations -import asyncio from collections.abc import Callable from functools import partial import logging @@ -86,16 +85,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), ) - async def finish() -> None: - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS_WITH_ENTRY_SUPPORT - ) - ) - await finish_setup(hass, entry, gateway) - - hass.async_create_task(finish()) + await hass.config_entries.async_forward_entry_setups( + entry, PLATFORMS_WITH_ENTRY_SUPPORT + ) + await finish_setup(hass, entry, gateway) return True diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index 7c8e0080bc2..8abf7bbd1fa 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -8,6 +8,7 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_ip": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441", "invalid_port": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442", diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index 433714de477..e1d9c10b053 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -34,7 +34,7 @@ "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si (subscribe) topik", "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3", "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", - "not_a_number": "K\u00e9rj\u00fck, adja meg a sz\u00e1mot", + "not_a_number": "K\u00e9rem, sz\u00e1mot adjon meg", "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie", "same_topic": "A feliratkoz\u00e1s \u00e9s a k\u00f6zz\u00e9t\u00e9tel t\u00e9m\u00e1i ugyanazok", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index da0354fe3c7..4a73363bf16 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -33,7 +33,7 @@ "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", "invalid_subscribe_topic": "\u7121\u52b9\u306a\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6 \u30c8\u30d4\u30c3\u30af", "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", - "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "mqtt_required": "MQTT\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "same_topic": "\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6\u3068\u30d1\u30d6\u30ea\u30c3\u30b7\u30e5\u306e\u30c8\u30d4\u30c3\u30af\u304c\u540c\u3058\u3067\u3059", diff --git a/homeassistant/components/mysensors/translations/pt.json b/homeassistant/components/mysensors/translations/pt.json new file mode 100644 index 00000000000..3ace45dd942 --- /dev/null +++ b/homeassistant/components/mysensors/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 021b46e2f38..25615db6eed 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -30,7 +30,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( ATTR_SDS011, ATTR_SPS30, - DEFAULT_NAME, DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER, @@ -66,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove air_quality entities from registry if they exist ent_reg = entity_registry.async_get(hass) @@ -130,7 +129,7 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): """Return the device info.""" return DeviceInfo( connections={(CONNECTION_NETWORK_MAC, cast(str, self._unique_id))}, - name=DEFAULT_NAME, + name="Nettigo Air Monitor", sw_version=self.nam.software_version, manufacturer=MANUFACTURER, configuration_url=f"http://{self.nam.host}/", diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index db5474ec925..6725ef3292d 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NAMDataUpdateCoordinator -from .const import DEFAULT_NAME, DOMAIN +from .const import DOMAIN PARALLEL_UPDATES = 1 @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription( key="restart", - name=f"{DEFAULT_NAME} Restart", + name="Restart", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, ) @@ -44,6 +44,8 @@ async def async_setup_entry( class NAMButton(CoordinatorEntity[NAMDataUpdateCoordinator], ButtonEntity): """Define an Nettigo Air Monitor button.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NAMDataUpdateCoordinator, diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index b81e7337a9f..e7c6f2532ef 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -4,21 +4,6 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, - PERCENTAGE, - PRESSURE_HPA, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_CELSIUS, -) -from homeassistant.helpers.entity import EntityCategory - SUFFIX_P0: Final = "_p0" SUFFIX_P1: Final = "_p1" SUFFIX_P2: Final = "_p2" @@ -49,7 +34,6 @@ ATTR_SPS30_P2: Final = f"{ATTR_SPS30}{SUFFIX_P2}" ATTR_SPS30_P4: Final = f"{ATTR_SPS30}{SUFFIX_P4}" ATTR_UPTIME: Final = "uptime" -DEFAULT_NAME: Final = "Nettigo Air Monitor" DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) DOMAIN: Final = "nam" MANUFACTURER: Final = "Nettigo" @@ -58,162 +42,3 @@ MIGRATION_SENSORS: Final = [ ("temperature", ATTR_DHT22_TEMPERATURE), ("humidity", ATTR_DHT22_HUMIDITY), ] - -SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( - SensorEntityDescription( - key=ATTR_BME280_HUMIDITY, - name=f"{DEFAULT_NAME} BME280 Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BME280_PRESSURE, - name=f"{DEFAULT_NAME} BME280 Pressure", - native_unit_of_measurement=PRESSURE_HPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BME280_TEMPERATURE, - name=f"{DEFAULT_NAME} BME280 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP180_PRESSURE, - name=f"{DEFAULT_NAME} BMP180 Pressure", - native_unit_of_measurement=PRESSURE_HPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP180_TEMPERATURE, - name=f"{DEFAULT_NAME} BMP180 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP280_PRESSURE, - name=f"{DEFAULT_NAME} BMP280 Pressure", - native_unit_of_measurement=PRESSURE_HPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP280_TEMPERATURE, - name=f"{DEFAULT_NAME} BMP280 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_HECA_HUMIDITY, - name=f"{DEFAULT_NAME} HECA Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_HECA_TEMPERATURE, - name=f"{DEFAULT_NAME} HECA Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_MHZ14A_CARBON_DIOXIDE, - name=f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.CO2, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SDS011_P1, - name=f"{DEFAULT_NAME} SDS011 Particulate Matter 10", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM10, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SDS011_P2, - name=f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM25, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SHT3X_HUMIDITY, - name=f"{DEFAULT_NAME} SHT3X Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SHT3X_TEMPERATURE, - name=f"{DEFAULT_NAME} SHT3X Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P0, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM1, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P1, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 10", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM10, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P2, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM25, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P4, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:molecule", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_DHT22_HUMIDITY, - name=f"{DEFAULT_NAME} DHT22 Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_DHT22_TEMPERATURE, - name=f"{DEFAULT_NAME} DHT22 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SIGNAL_STRENGTH, - name=f"{DEFAULT_NAME} Signal Strength", - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_UPTIME, - name=f"{DEFAULT_NAME} Uptime", - device_class=SensorDeviceClass.TIMESTAMP, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), -) diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index af729cf9066..6229102035e 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -7,24 +7,219 @@ from typing import cast from homeassistant.components.sensor import ( DOMAIN as PLATFORM, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow from . import NAMDataUpdateCoordinator -from .const import ATTR_UPTIME, DOMAIN, MIGRATION_SENSORS, SENSORS +from .const import ( + ATTR_BME280_HUMIDITY, + ATTR_BME280_PRESSURE, + ATTR_BME280_TEMPERATURE, + ATTR_BMP180_PRESSURE, + ATTR_BMP180_TEMPERATURE, + ATTR_BMP280_PRESSURE, + ATTR_BMP280_TEMPERATURE, + ATTR_DHT22_HUMIDITY, + ATTR_DHT22_TEMPERATURE, + ATTR_HECA_HUMIDITY, + ATTR_HECA_TEMPERATURE, + ATTR_MHZ14A_CARBON_DIOXIDE, + ATTR_SDS011_P1, + ATTR_SDS011_P2, + ATTR_SHT3X_HUMIDITY, + ATTR_SHT3X_TEMPERATURE, + ATTR_SIGNAL_STRENGTH, + ATTR_SPS30_P0, + ATTR_SPS30_P1, + ATTR_SPS30_P2, + ATTR_SPS30_P4, + ATTR_UPTIME, + DOMAIN, + MIGRATION_SENSORS, +) PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=ATTR_BME280_HUMIDITY, + name="BME280 humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BME280_PRESSURE, + name="BME280 pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BME280_TEMPERATURE, + name="BME280 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP180_PRESSURE, + name="BMP180 pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP180_TEMPERATURE, + name="BMP180 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP280_PRESSURE, + name="BMP280 pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP280_TEMPERATURE, + name="BMP280 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_HECA_HUMIDITY, + name="HECA humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_HECA_TEMPERATURE, + name="HECA temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_MHZ14A_CARBON_DIOXIDE, + name="MH-Z14A carbon dioxide", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SDS011_P1, + name="SDS011 particulate matter 10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SDS011_P2, + name="SDS011 particulate matter 2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SHT3X_HUMIDITY, + name="SHT3X humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SHT3X_TEMPERATURE, + name="SHT3X temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P0, + name="SPS30 particulate matter 1.0", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM1, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P1, + name="SPS30 particulate matter 10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P2, + name="SPS30 particulate matter 2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P4, + name="SPS30 particulate matter 4.0", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:molecule", + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_DHT22_HUMIDITY, + name="DHT22 humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_DHT22_TEMPERATURE, + name="DHT22 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SIGNAL_STRENGTH, + name="Signal strength", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_UPTIME, + name="Uptime", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -61,6 +256,8 @@ async def async_setup_entry( class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): """Define an Nettigo Air Monitor sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NAMDataUpdateCoordinator, diff --git a/homeassistant/components/nam/translations/cs.json b/homeassistant/components/nam/translations/cs.json index 72df4a96818..1b979ab1412 100644 --- a/homeassistant/components/nam/translations/cs.json +++ b/homeassistant/components/nam/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index 2125c9e3e38..d5147344c61 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -34,7 +34,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Nettigo Air Monitor\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "description": "Nettigo Air Monitor\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/nam/translations/pt.json b/homeassistant/components/nam/translations/pt.json new file mode 100644 index 00000000000..0aa6df94840 --- /dev/null +++ b/homeassistant/components/nam/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "credentials": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "reauth_confirm": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index f6fb2f8112b..1d18e0078ec 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nanoleaf, coordinator, event_listener ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nanoleaf/translations/pt.json b/homeassistant/components/nanoleaf/translations/pt.json new file mode 100644 index 00000000000..7293cf1c3c3 --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index cbfd860a0b1..e7b402aed36 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[NEATO_LOGIN] = hub - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index b31354b598c..44658558b62 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -29,6 +29,11 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.camera import Image, img_util from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.repairs import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, @@ -187,6 +192,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, unique_id=entry.data[CONF_PROJECT_ID] ) + async_delete_issue(hass, DOMAIN, "removed_app_auth") + subscriber = await api.new_subscriber(hass, entry) if not subscriber: return False @@ -227,7 +234,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_DEVICE_MANAGER: device_manager, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -255,6 +262,18 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN: # App Auth credentials have been deprecated and must be re-created # by the user in the config flow + async_create_issue( + hass, + DOMAIN, + "removed_app_auth", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="removed_app_auth", + translation_placeholders={ + "more_info_url": "https://www.home-assistant.io/more-info/nest-auth-deprecation", + "documentation_url": "https://www.home-assistant.io/integrations/nest/", + }, + ) raise ConfigEntryAuthFailed( "Google has deprecated App Auth credentials, and the integration " "must be reconfigured in the UI to restore access to Nest Devices." @@ -271,12 +290,14 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: WEB_AUTH_DOMAIN, ) - _LOGGER.warning( - "Configuration of Nest integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " - "(including OAuth Application Credentials) has been imported into " - "the UI automatically and can be safely removed from your " - "configuration.yaml file" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", ) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 4e38338aee8..c26b216b21f 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -61,6 +61,8 @@ async def async_setup_sdm_entry( class NestCamera(Camera): """Devices that support cameras.""" + _attr_has_entity_name = True + def __init__(self, device: Device) -> None: """Initialize the camera.""" super().__init__() @@ -83,11 +85,6 @@ class NestCamera(Camera): # The API "name" field is a unique device identifier. return f"{self._device.name}-camera" - @property - def name(self) -> str | None: - """Return the name of the camera.""" - return self._device_info.device_name - @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 452c30073da..c13370776ad 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -97,6 +97,7 @@ class ThermostatEntity(ClimateEntity): _attr_min_temp = MIN_TEMP _attr_max_temp = MAX_TEMP + _attr_has_entity_name = True def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" @@ -115,11 +116,6 @@ class ThermostatEntity(ClimateEntity): # The API "name" field is a unique device identifier. return self._device.name - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._device_info.device_name - @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 72e0aed8420..d826272b207 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "application_credentials"], + "dependencies": ["ffmpeg", "http", "application_credentials", "repairs"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"], diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 4614d4b1ed4..7b5b96b7145 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -22,6 +22,7 @@ from collections.abc import Mapping from dataclasses import dataclass import logging import os +from typing import Any from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait from google_nest_sdm.device import Device @@ -89,7 +90,7 @@ async def async_get_media_event_store( os.makedirs(media_path, exist_ok=True) await hass.async_add_executor_job(mkdir) - store = Store(hass, STORAGE_VERSION, STORAGE_KEY, private=True) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY, private=True) return NestEventMediaStore(hass, subscriber, store, media_path) @@ -119,7 +120,7 @@ class NestEventMediaStore(EventMediaStore): self, hass: HomeAssistant, subscriber: GoogleNestSubscriber, - store: Store, + store: Store[dict[str, Any]], media_path: str, ) -> None: """Initialize NestEventMediaStore.""" @@ -127,7 +128,7 @@ class NestEventMediaStore(EventMediaStore): self._subscriber = subscriber self._store = store self._media_path = media_path - self._data: dict | None = None + self._data: dict[str, Any] | None = None self._devices: Mapping[str, str] | None = {} async def async_load(self) -> dict | None: @@ -137,15 +138,9 @@ class NestEventMediaStore(EventMediaStore): if (data := await self._store.async_load()) is None: _LOGGER.debug("Loaded empty event store") self._data = {} - elif isinstance(data, dict): + else: _LOGGER.debug("Loaded event store with %d records", len(data)) self._data = data - else: - raise ValueError( - "Unexpected data in storage version={}, key={}".format( - STORAGE_VERSION, STORAGE_KEY - ) - ) return self._data async def async_save(self, data: dict) -> None: diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index c6d1c8b2b30..11edc9f3506 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -53,6 +53,7 @@ class SensorBase(SensorEntity): _attr_should_poll = False _attr_state_class = SensorStateClass.MEASUREMENT + _attr_has_entity_name = True def __init__(self, device: Device) -> None: """Initialize the sensor.""" @@ -73,11 +74,7 @@ class TemperatureSensor(SensorBase): _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device_info.device_name} Temperature" + _attr_name = "Temperature" @property def native_value(self) -> float: @@ -94,11 +91,7 @@ class HumiditySensor(SensorBase): _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device_info.device_name} Humidity" + _attr_name = "Humidity" @property def native_value(self) -> int: diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 0a13de41511..07ba63ac479 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -88,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Nest YAML configuration is being removed", + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_app_auth": { + "title": "Nest Authentication Credentials must be updated", + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information." + } } } diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 5f026e55f31..cd8274d635a 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -10,7 +10,6 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", - "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)" }, "step": { - "auth": { - "data": { - "code": "Access Token" - }, - "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", - "title": "Link Google Account" - }, "auth_upgrade": { "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", "title": "Nest: App Auth Deprecation" @@ -96,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Nest YAML configuration is being removed" + }, + "removed_app_auth": { + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information.", + "title": "Nest Authentication Credentials must be updated" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index ee9fcbba98d..55d9b9a0348 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -7,7 +7,7 @@ "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "create_entry": { @@ -37,6 +37,7 @@ "data": { "cloud_project_id": "Google Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID" }, + "description": "\u4ee5\u4e0b\u306bCloud Project ID\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u4f8b: *example-project-12345*\u3002[Google Cloud Console]({cloud_console_url}) \u307e\u305f\u306f[\u8a73\u7d30]({more_info_url})\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30cd\u30b9\u30c8: \u30af\u30e9\u30a6\u30c9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID\u3092\u5165\u529b" }, "create_cloud_project": { @@ -76,8 +77,8 @@ "title": "Google Cloud\u306e\u8a2d\u5b9a" }, "reauth_confirm": { - "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Nest\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 13a7439b93d..40b70a2c67f 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", @@ -14,9 +16,14 @@ "internal_error": "Erro interno ao validar o c\u00f3digo", "invalid_pin": "C\u00f3digo PIN inv\u00e1lido", "timeout": "Limite temporal ultrapassado ao validar c\u00f3digo", - "unknown": "Erro desconhecido ao validar o c\u00f3digo" + "unknown": "Erro inesperado" }, "step": { + "auth": { + "data": { + "code": "Token de Acesso" + } + }, "init": { "data": { "flow_impl": "Fornecedor" @@ -33,6 +40,9 @@ }, "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index f17f48b4560..e071637713f 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -1,6 +1,10 @@ { + "application_credentials": { + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}), \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Cloud Console: \n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 OAuth]({oauth_consent_url}) \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u043d\u0430 \u043d\u0435\u0439 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f.\n2. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n3. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n4. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web Application** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0442\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\n5. \u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 `{redirect_url}` \u0432 \u043f\u043e\u043b\u0435 *Authorized redirect URI*." + }, "config": { "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", @@ -19,7 +23,7 @@ "subscriber_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0433\u043e \u043f\u043e\u0434\u043f\u0438\u0441\u0447\u0438\u043a\u0430. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0445.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", - "wrong_project_id": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 Cloud Project ID (\u043d\u0430\u0439\u0434\u0435\u043d Device Access Project ID)" + "wrong_project_id": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 Cloud Project ID (\u0442\u0430\u043a\u043e\u0439 \u0436\u0435 \u043a\u0430\u043a \u0438 Device Access Project ID)" }, "step": { "auth": { @@ -29,6 +33,32 @@ "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Google. \n\n\u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0442\u043e\u043a\u0435\u043d.", "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 Google" }, + "auth_upgrade": { + "description": "\u041f\u0440\u0435\u0436\u043d\u0438\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0431\u044b\u043b \u0443\u043f\u0440\u0430\u0437\u0434\u043d\u0435\u043d Google \u0434\u043b\u044f \u043f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438. \u0412\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\n\n\u0427\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u043b\u044f \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c Nest \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438]({more_info_url}).", + "title": "Nest: \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 *example-project-12345*. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 [Google Cloud Console({cloud_console_url}) \u0438\u043b\u0438 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e]({more_info_url}).", + "title": "Nest: \u0432\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID" + }, + "create_cloud_project": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Nest \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u044b, \u043a\u0430\u043c\u0435\u0440\u044b \u0438 \u0434\u0432\u0435\u0440\u043d\u044b\u0435 \u0437\u0432\u043e\u043d\u043a\u0438 Nest \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Smart Device Management. SDM API **\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0435\u0434\u0438\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043f\u043b\u0430\u0442\u044b \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 US $5**. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({more_info_url}).\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [Google Cloud Console]({cloud_console_url}).\n2. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u0412\u0430\u0448 \u043f\u0435\u0440\u0432\u044b\u0439 \u043f\u0440\u043e\u0435\u043a\u0442, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Create Project**, \u0437\u0430\u0442\u0435\u043c **New Project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Create**.\n4. \u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u0435 Cloud Project ID, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, *example-project-12345*, \u043e\u043d \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0412\u0430\u043c \u043f\u043e\u0437\u0436\u0435.\n5. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 API \u0434\u043b\u044f [Smart Device Management API]({sdm_api_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Enable**.\n6. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 API \u0434\u043b\u044f [Cloud Pub/Sub API]({pubsub_api_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Enable**.\n\n\u041f\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044e \u0432\u044b\u0448\u0435\u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0448\u0430\u0433\u043e\u0432 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.", + "title": "Nest: \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430" + }, + "device_project": { + "data": { + "project_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e **\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043b\u0430\u0442\u0430 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 5 \u0434\u043e\u043b\u043b\u0430\u0440\u043e\u0432 \u0421\u0428\u0410**.\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u041a\u043e\u043d\u0441\u043e\u043b\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c]({device_access_console_url}) \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043e\u043f\u043b\u0430\u0442\u044b.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 **Create project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Next**.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth.\n5. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043d\u0430\u0436\u0430\u0432 **Enable** \u0438 **Create project**. \n\n\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c \u043d\u0438\u0436\u0435 ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).", + "title": "Nest: \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "device_project_upgrade": { + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0443\u043a\u0430\u0437\u0430\u0432 \u043d\u043e\u0432\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u041a\u043e\u043d\u0441\u043e\u043b\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c]({device_access_console_url}).\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a \u043a\u043e\u0440\u0437\u0438\u043d\u044b \u0440\u044f\u0434\u043e\u043c \u0441 *OAuth Client ID*.\n3. \u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043c\u0435\u043d\u044e, \u043d\u0430\u0436\u0430\u0432 \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0443 `...`, \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 *Add Client ID*.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Add**. \n\n\u0412\u0430\u0448 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth: `{client_id}`", + "title": "Nest: \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c" + }, "init": { "data": { "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index 1732443184e..bc69b928ba1 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -23,7 +23,7 @@ "subscriber_error": "Bilinmeyen abone hatas\u0131, g\u00fcnl\u00fcklere bak\u0131n\u0131z", "timeout": "Zaman a\u015f\u0131m\u0131 do\u011frulama kodu", "unknown": "Beklenmeyen hata", - "wrong_project_id": "L\u00fctfen ge\u00e7erli bir Bulut Projesi Kimli\u011fi girin (bulunan Ayg\u0131t Eri\u015fimi Proje Kimli\u011fi)" + "wrong_project_id": "L\u00fctfen ge\u00e7erli bir Bulut Projesi Kimli\u011fi girin (Cihaz Eri\u015fimi Proje Kimli\u011fi ile ayn\u0131yd\u0131)" }, "step": { "auth": { diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index a3a9d22e017..36847c85515 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -153,7 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await data_handler.async_setup() hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def unregister_webhook( call_or_event_or_dt: ServiceCall | Event | datetime | None, diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index e5eaad50c9f..3235d16479c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -112,6 +112,8 @@ async def async_setup_entry( class NetatmoCamera(NetatmoBase, Camera): """Representation of a Netatmo camera.""" + _attr_brand = MANUFACTURER + _attr_has_entity_name = True _attr_supported_features = CameraEntityFeature.STREAM def __init__( @@ -126,14 +128,13 @@ class NetatmoCamera(NetatmoBase, Camera): Camera.__init__(self) super().__init__(data_handler) - self._data_classes.append( + self._publishers.append( {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} ) self._id = camera_id self._home_id = home_id self._device_name = self._data.get_camera(camera_id=camera_id)["name"] - self._attr_name = f"{MANUFACTURER} {self._device_name}" self._model = camera_type self._netatmo_type = TYPE_SECURITY self._attr_unique_id = f"{self._id}-{self._model}" @@ -193,7 +194,7 @@ class NetatmoCamera(NetatmoBase, Camera): """Return data for this entity.""" return cast( pyatmo.AsyncCameraData, - self.data_handler.data[self._data_classes[0]["name"]], + self.data_handler.data[self._publishers[0]["name"]], ) async def async_camera_image( @@ -219,11 +220,6 @@ class NetatmoCamera(NetatmoBase, Camera): """Return True if entity is available.""" return bool(self._alim_status == "on" or self._status == "disconnected") - @property - def brand(self) -> str: - """Return the camera brand.""" - return MANUFACTURER - @property def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index cc515e50a11..eb7d996eefb 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -127,7 +127,7 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( + await data_handler.subscribe( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) @@ -185,14 +185,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._room = room self._id = self._room.entity_id - self._climate_state_class = ( - f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" - ) - self._climate_state: pyatmo.AsyncClimate = data_handler.data[ - self._climate_state_class - ] + self._signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" + self._climate_state: pyatmo.AsyncClimate = data_handler.data[self._signal_name] - self._data_classes.extend( + self._publishers.extend( [ { "name": CLIMATE_TOPOLOGY_CLASS_NAME, @@ -201,7 +197,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): { "name": CLIMATE_STATE_CLASS_NAME, "home_id": self._room.home.entity_id, - SIGNAL_NAME: self._climate_state_class, + SIGNAL_NAME: self._signal_name, }, ] ) @@ -254,7 +250,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.data_handler, module, self._id, - self._climate_state_class, + self._signal_name, ), ) @@ -278,7 +274,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ATTR_SELECTED_SCHEDULE ] = self._selected_schedule self.async_write_ha_state() - self.data_handler.async_force_update(self._climate_state_class) + self.data_handler.async_force_update(self._signal_name) return home = data["home"] @@ -295,7 +291,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._attr_target_temperature = self._away_temperature elif self._attr_preset_mode == PRESET_SCHEDULE: self.async_update_callback() - self.data_handler.async_force_update(self._climate_state_class) + self.data_handler.async_force_update(self._signal_name) self.async_write_ha_state() return diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 5fda8759540..6bd66fa9644 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -73,16 +73,16 @@ DATA_HANDLER = "netatmo_data_handler" SIGNAL_NAME = "signal_name" NETATMO_CREATE_BATTERY = "netatmo_create_battery" -CONF_CLOUDHOOK_URL = "cloudhook_url" -CONF_WEATHER_AREAS = "weather_areas" -CONF_NEW_AREA = "new_area" CONF_AREA_NAME = "area_name" +CONF_CLOUDHOOK_URL = "cloudhook_url" CONF_LAT_NE = "lat_ne" -CONF_LON_NE = "lon_ne" CONF_LAT_SW = "lat_sw" +CONF_LON_NE = "lon_ne" CONF_LON_SW = "lon_sw" +CONF_NEW_AREA = "new_area" CONF_PUBLIC_MODE = "mode" CONF_UUID = "uuid" +CONF_WEATHER_AREAS = "weather_areas" OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" @@ -94,53 +94,53 @@ DATA_HOMES = "netatmo_homes" DATA_PERSONS = "netatmo_persons" DATA_SCHEDULES = "netatmo_schedules" -NETATMO_WEBHOOK_URL = None NETATMO_EVENT = "netatmo_event" +NETATMO_WEBHOOK_URL = None -DEFAULT_PERSON = "unknown" DEFAULT_DISCOVERY = True +DEFAULT_PERSON = "unknown" DEFAULT_WEBHOOKS = False -ATTR_PSEUDO = "pseudo" +ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" ATTR_EVENT_TYPE = "event_type" +ATTR_FACE_URL = "face_url" ATTR_HEATING_POWER_REQUEST = "heating_power_request" ATTR_HOME_ID = "home_id" ATTR_HOME_NAME = "home_name" +ATTR_IS_KNOWN = "is_known" ATTR_PERSON = "person" ATTR_PERSONS = "persons" -ATTR_IS_KNOWN = "is_known" -ATTR_FACE_URL = "face_url" +ATTR_PSEUDO = "pseudo" ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_NAME = "schedule_name" ATTR_SELECTED_SCHEDULE = "selected_schedule" -ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" SERVICE_SET_CAMERA_LIGHT = "set_camera_light" -SERVICE_SET_SCHEDULE = "set_schedule" -SERVICE_SET_PERSONS_HOME = "set_persons_home" SERVICE_SET_PERSON_AWAY = "set_person_away" +SERVICE_SET_PERSONS_HOME = "set_persons_home" +SERVICE_SET_SCHEDULE = "set_schedule" # Climate events -EVENT_TYPE_SET_POINT = "set_point" EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" -EVENT_TYPE_THERM_MODE = "therm_mode" EVENT_TYPE_SCHEDULE = "schedule" +EVENT_TYPE_SET_POINT = "set_point" +EVENT_TYPE_THERM_MODE = "therm_mode" # Camera events -EVENT_TYPE_LIGHT_MODE = "light_mode" -EVENT_TYPE_CAMERA_OUTDOOR = "outdoor" EVENT_TYPE_CAMERA_ANIMAL = "animal" EVENT_TYPE_CAMERA_HUMAN = "human" -EVENT_TYPE_CAMERA_VEHICLE = "vehicle" EVENT_TYPE_CAMERA_MOVEMENT = "movement" +EVENT_TYPE_CAMERA_OUTDOOR = "outdoor" EVENT_TYPE_CAMERA_PERSON = "person" EVENT_TYPE_CAMERA_PERSON_AWAY = "person_away" +EVENT_TYPE_CAMERA_VEHICLE = "vehicle" +EVENT_TYPE_LIGHT_MODE = "light_mode" # Door tags -EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move" +EVENT_TYPE_ALARM_STARTED = "alarm_started" EVENT_TYPE_DOOR_TAG_BIG_MOVE = "tag_big_move" EVENT_TYPE_DOOR_TAG_OPEN = "tag_open" +EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move" EVENT_TYPE_OFF = "off" EVENT_TYPE_ON = "on" -EVENT_TYPE_ALARM_STARTED = "alarm_started" OUTDOOR_CAMERA_TRIGGERS = [ EVENT_TYPE_CAMERA_ANIMAL, @@ -149,46 +149,46 @@ OUTDOOR_CAMERA_TRIGGERS = [ EVENT_TYPE_CAMERA_VEHICLE, ] INDOOR_CAMERA_TRIGGERS = [ - EVENT_TYPE_CAMERA_MOVEMENT, - EVENT_TYPE_CAMERA_PERSON, - EVENT_TYPE_CAMERA_PERSON_AWAY, EVENT_TYPE_ALARM_STARTED, + EVENT_TYPE_CAMERA_MOVEMENT, + EVENT_TYPE_CAMERA_PERSON_AWAY, + EVENT_TYPE_CAMERA_PERSON, ] DOOR_TAG_TRIGGERS = [ - EVENT_TYPE_DOOR_TAG_SMALL_MOVE, EVENT_TYPE_DOOR_TAG_BIG_MOVE, EVENT_TYPE_DOOR_TAG_OPEN, + EVENT_TYPE_DOOR_TAG_SMALL_MOVE, ] CLIMATE_TRIGGERS = [ - EVENT_TYPE_SET_POINT, EVENT_TYPE_CANCEL_SET_POINT, + EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, ] EVENT_ID_MAP = { - EVENT_TYPE_CAMERA_MOVEMENT: "device_id", - EVENT_TYPE_CAMERA_PERSON: "device_id", - EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id", + EVENT_TYPE_ALARM_STARTED: "device_id", EVENT_TYPE_CAMERA_ANIMAL: "device_id", EVENT_TYPE_CAMERA_HUMAN: "device_id", + EVENT_TYPE_CAMERA_MOVEMENT: "device_id", EVENT_TYPE_CAMERA_OUTDOOR: "device_id", + EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id", + EVENT_TYPE_CAMERA_PERSON: "device_id", EVENT_TYPE_CAMERA_VEHICLE: "device_id", - EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id", + EVENT_TYPE_CANCEL_SET_POINT: "room_id", EVENT_TYPE_DOOR_TAG_BIG_MOVE: "device_id", EVENT_TYPE_DOOR_TAG_OPEN: "device_id", + EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id", EVENT_TYPE_LIGHT_MODE: "device_id", - EVENT_TYPE_ALARM_STARTED: "device_id", - EVENT_TYPE_CANCEL_SET_POINT: "room_id", EVENT_TYPE_SET_POINT: "room_id", EVENT_TYPE_THERM_MODE: "home_id", } -MODE_LIGHT_ON = "on" -MODE_LIGHT_OFF = "off" MODE_LIGHT_AUTO = "auto" +MODE_LIGHT_OFF = "off" +MODE_LIGHT_ON = "on" CAMERA_LIGHT_MODES = [MODE_LIGHT_ON, MODE_LIGHT_OFF, MODE_LIGHT_AUTO] WEBHOOK_ACTIVATION = "webhook_activation" WEBHOOK_DEACTIVATION = "webhook_deactivation" +WEBHOOK_LIGHT_MODE = "NOC-light_mode" WEBHOOK_NACAMERA_CONNECTION = "NACamera-connection" WEBHOOK_PUSH_TYPE = "push_type" -WEBHOOK_LIGHT_MODE = "NOC-light_mode" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 1d6345506c1..3a1ea73e311 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -64,11 +64,11 @@ class NetatmoDevice: data_handler: NetatmoDataHandler device: pyatmo.climate.NetatmoModule parent_id: str - state_class_name: str + signal_name: str @dataclass -class NetatmoDataClass: +class NetatmoPublisher: """Class for keeping track of Netatmo data class metadata.""" name: str @@ -85,7 +85,7 @@ class NetatmoDataHandler: self.hass = hass self.config_entry = config_entry self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH] - self.data_classes: dict = {} + self.publisher: dict[str, NetatmoPublisher] = {} self.data: dict = {} self._queue: deque = deque() self._webhook: bool = False @@ -107,7 +107,7 @@ class NetatmoDataHandler: await asyncio.gather( *[ - self.register_data_class(data_class, data_class, None) + self.subscribe(data_class, data_class, None) for data_class in ( CLIMATE_TOPOLOGY_CLASS_NAME, CAMERA_DATA_CLASS_NAME, @@ -128,20 +128,18 @@ class NetatmoDataHandler: if data_class.next_scan > time(): continue - if data_class_name := data_class.name: - self.data_classes[data_class_name].next_scan = ( - time() + data_class.interval - ) + if publisher := data_class.name: + self.publisher[publisher].next_scan = time() + data_class.interval - await self.async_fetch_data(data_class_name) + await self.async_fetch_data(publisher) self._queue.rotate(BATCH_SIZE) @callback - def async_force_update(self, data_class_entry: str) -> None: + def async_force_update(self, signal_name: str) -> None: """Prioritize data retrieval for given data class entry.""" - self.data_classes[data_class_entry].next_scan = time() - self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) + self.publisher[signal_name].next_scan = time() + self._queue.rotate(-(self._queue.index(self.publisher[signal_name]))) async def handle_event(self, event: dict) -> None: """Handle webhook events.""" @@ -157,17 +155,17 @@ class NetatmoDataHandler: _LOGGER.debug("%s camera reconnected", MANUFACTURER) self.async_force_update(CAMERA_DATA_CLASS_NAME) - async def async_fetch_data(self, data_class_entry: str) -> None: + async def async_fetch_data(self, signal_name: str) -> None: """Fetch data and notify.""" - if self.data[data_class_entry] is None: + if self.data[signal_name] is None: return try: - await self.data[data_class_entry].async_update() + await self.data[signal_name].async_update() except pyatmo.NoDevice as err: _LOGGER.debug(err) - self.data[data_class_entry] = None + self.data[signal_name] = None except pyatmo.ApiError as err: _LOGGER.debug(err) @@ -176,56 +174,52 @@ class NetatmoDataHandler: _LOGGER.debug(err) return - for update_callback in self.data_classes[data_class_entry].subscriptions: + for update_callback in self.publisher[signal_name].subscriptions: if update_callback: update_callback() - async def register_data_class( + async def subscribe( self, - data_class_name: str, - data_class_entry: str, + publisher: str, + signal_name: str, update_callback: CALLBACK_TYPE | None, **kwargs: Any, ) -> None: - """Register data class.""" - if data_class_entry in self.data_classes: - if update_callback not in self.data_classes[data_class_entry].subscriptions: - self.data_classes[data_class_entry].subscriptions.append( - update_callback - ) + """Subscribe to publisher.""" + if signal_name in self.publisher: + if update_callback not in self.publisher[signal_name].subscriptions: + self.publisher[signal_name].subscriptions.append(update_callback) return - self.data_classes[data_class_entry] = NetatmoDataClass( - name=data_class_entry, - interval=DEFAULT_INTERVALS[data_class_name], - next_scan=time() + DEFAULT_INTERVALS[data_class_name], + self.publisher[signal_name] = NetatmoPublisher( + name=signal_name, + interval=DEFAULT_INTERVALS[publisher], + next_scan=time() + DEFAULT_INTERVALS[publisher], subscriptions=[update_callback], ) - self.data[data_class_entry] = DATA_CLASSES[data_class_name]( - self._auth, **kwargs - ) + self.data[signal_name] = DATA_CLASSES[publisher](self._auth, **kwargs) try: - await self.async_fetch_data(data_class_entry) + await self.async_fetch_data(signal_name) except KeyError: - self.data_classes.pop(data_class_entry) + self.publisher.pop(signal_name) raise - self._queue.append(self.data_classes[data_class_entry]) - _LOGGER.debug("Data class %s added", data_class_entry) + self._queue.append(self.publisher[signal_name]) + _LOGGER.debug("Publisher %s added", signal_name) - async def unregister_data_class( - self, data_class_entry: str, update_callback: CALLBACK_TYPE | None + async def unsubscribe( + self, signal_name: str, update_callback: CALLBACK_TYPE | None ) -> None: - """Unregister data class.""" - self.data_classes[data_class_entry].subscriptions.remove(update_callback) + """Unsubscribe from publisher.""" + self.publisher[signal_name].subscriptions.remove(update_callback) - if not self.data_classes[data_class_entry].subscriptions: - self._queue.remove(self.data_classes[data_class_entry]) - self.data_classes.pop(data_class_entry) - self.data.pop(data_class_entry) - _LOGGER.debug("Data class %s removed", data_class_entry) + if not self.publisher[signal_name].subscriptions: + self._queue.remove(self.publisher[signal_name]) + self.publisher.pop(signal_name) + self.data.pop(signal_name) + _LOGGER.debug("Publisher %s removed", signal_name) @property def webhook(self) -> bool: diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 7e078153a8a..6567ae770f2 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -17,7 +17,6 @@ from .const import ( DATA_HANDLER, DOMAIN, EVENT_TYPE_LIGHT_MODE, - MANUFACTURER, SIGNAL_NAME, TYPE_SECURITY, WEBHOOK_LIGHT_MODE, @@ -63,6 +62,7 @@ class NetatmoLight(NetatmoBase, LightEntity): """Representation of a Netatmo Presence camera light.""" _attr_color_mode = ColorMode.ONOFF + _attr_has_entity_name = True _attr_supported_color_modes = {ColorMode.ONOFF} def __init__( @@ -76,7 +76,7 @@ class NetatmoLight(NetatmoBase, LightEntity): LightEntity.__init__(self) super().__init__(data_handler) - self._data_classes.append( + self._publishers.append( {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} ) self._id = camera_id @@ -84,7 +84,6 @@ class NetatmoLight(NetatmoBase, LightEntity): self._model = camera_type self._netatmo_type = TYPE_SECURITY self._device_name: str = self._data.get_camera(camera_id)["name"] - self._attr_name = f"{MANUFACTURER} {self._device_name}" self._is_on = False self._attr_unique_id = f"{self._id}-light" @@ -123,7 +122,7 @@ class NetatmoLight(NetatmoBase, LightEntity): """Return data for this entity.""" return cast( pyatmo.AsyncCameraData, - self.data_handler.data[self._data_classes[0]["name"]], + self.data_handler.data[self._publishers[0]["name"]], ) @property diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index decedbbdfbd..e8a346ccd84 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -1,6 +1,8 @@ """Base class for Netatmo entities.""" from __future__ import annotations +from typing import Any + from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers import device_registry as dr @@ -23,7 +25,7 @@ class NetatmoBase(Entity): def __init__(self, data_handler: NetatmoDataHandler) -> None: """Set up Netatmo entity base.""" self.data_handler = data_handler - self._data_classes: list[dict] = [] + self._publishers: list[dict[str, Any]] = [] self._device_name: str = "" self._id: str = "" @@ -35,11 +37,11 @@ class NetatmoBase(Entity): async def async_added_to_hass(self) -> None: """Entity created.""" - for data_class in self._data_classes: + for data_class in self._publishers: signal_name = data_class[SIGNAL_NAME] if "home_id" in data_class: - await self.data_handler.register_data_class( + await self.data_handler.subscribe( data_class["name"], signal_name, self.async_update_callback, @@ -47,7 +49,7 @@ class NetatmoBase(Entity): ) elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME: - await self.data_handler.register_data_class( + await self.data_handler.subscribe( data_class["name"], signal_name, self.async_update_callback, @@ -58,13 +60,13 @@ class NetatmoBase(Entity): ) else: - await self.data_handler.register_data_class( + await self.data_handler.subscribe( data_class["name"], signal_name, self.async_update_callback ) - for sub in self.data_handler.data_classes[signal_name].subscriptions: + for sub in self.data_handler.publisher[signal_name].subscriptions: if sub is None: - await self.data_handler.unregister_data_class(signal_name, None) + await self.data_handler.unsubscribe(signal_name, None) registry = dr.async_get(self.hass) if device := registry.async_get_device({(DOMAIN, self._id)}): @@ -76,8 +78,8 @@ class NetatmoBase(Entity): """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() - for data_class in self._data_classes: - await self.data_handler.unregister_data_class( + for data_class in self._publishers: + await self.data_handler.unsubscribe( data_class[SIGNAL_NAME], self.async_update_callback ) diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 56f33b04432..62e6ef25969 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -46,7 +46,7 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( + await data_handler.subscribe( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) @@ -92,7 +92,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._home = self._climate_state.homes[self._home_id] - self._data_classes.extend( + self._publishers.extend( [ { "name": CLIMATE_TOPOLOGY_CLASS_NAME, diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 217b2146cc9..bec9af96442 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -41,7 +41,6 @@ from .const import ( CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, - MANUFACTURER, NETATMO_CREATE_BATTERY, SIGNAL_NAME, TYPE_WEATHER, @@ -422,7 +421,7 @@ async def async_setup_entry( ) continue - await data_handler.register_data_class( + await data_handler.subscribe( PUBLICDATA_DATA_CLASS_NAME, signal_name, None, @@ -487,9 +486,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): super().__init__(data_handler) self.entity_description = description - self._data_classes.append( - {"name": data_class_name, SIGNAL_NAME: data_class_name} - ) + self._publishers.append({"name": data_class_name, SIGNAL_NAME: data_class_name}) self._id = module_info["_id"] self._station_id = module_info.get("main_device", self._id) @@ -507,7 +504,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): f"{module_info.get('module_name', device['type'])}" ) - self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" + self._attr_name = f"{self._device_name} {description.name}" self._model = device["type"] self._netatmo_type = TYPE_WEATHER self._attr_unique_id = f"{self._id}-{description.key}" @@ -517,7 +514,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): """Return data for this entity.""" return cast( pyatmo.AsyncWeatherStationData, - self.data_handler.data[self._data_classes[0]["name"]], + self.data_handler.data[self._publishers[0]["name"]], ) @property @@ -598,7 +595,7 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): self._id = netatmo_device.parent_id self._attr_name = f"{self._module.name} {self.entity_description.name}" - self._state_class_name = netatmo_device.state_class_name + self._signal_name = netatmo_device.signal_name self._room_id = self._module.room_id self._model = getattr(self._module.device_type, "value") @@ -734,7 +731,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - self._data_classes.append( + self._publishers.append( { "name": PUBLICDATA_DATA_CLASS_NAME, "lat_ne": area.lat_ne, @@ -751,7 +748,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._area_name = area.area_name self._id = self._area_name self._device_name = f"{self._area_name}" - self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" + self._attr_name = f"{self._device_name} {description.name}" self._show_on_map = area.show_on_map self._attr_unique_id = ( f"{self._device_name.replace(' ', '-')}-{description.key}" @@ -788,13 +785,13 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): if self.area == area: return - await self.data_handler.unregister_data_class( + await self.data_handler.unsubscribe( self._signal_name, self.async_update_callback ) self.area = area self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - self._data_classes = [ + self._publishers = [ { "name": PUBLICDATA_DATA_CLASS_NAME, "lat_ne": area.lat_ne, @@ -807,7 +804,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): ] self._mode = area.mode self._show_on_map = area.show_on_map - await self.data_handler.register_data_class( + await self.data_handler.subscribe( PUBLICDATA_DATA_CLASS_NAME, self._signal_name, self.async_update_callback, diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index b9f2f17960a..c60eb3dcd5e 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -5,7 +5,7 @@ "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" @@ -15,8 +15,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Netatmo\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/netatmo/translations/pt.json b/homeassistant/components/netatmo/translations/pt.json index e39ecffa8a7..191337f9812 100644 --- a/homeassistant/components/netatmo/translations/pt.json +++ b/homeassistant/components/netatmo/translations/pt.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "create_entry": { @@ -12,6 +13,9 @@ "step": { "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index a996699ab9e..ade5a8df6bd 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -160,7 +160,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: KEY_COORDINATOR_LINK: coordinator_link, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index eaa32362baf..cf6cc827519 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -78,7 +78,7 @@ DEVICE_ICONS = { 1: "mdi:book-open-variant", # Amazon Kindle 2: "mdi:android", # Android Device 3: "mdi:cellphone", # Android Phone - 4: "mdi:tablet-android", # Android Tablet + 4: "mdi:tablet", # Android Tablet 5: "mdi:router-wireless", # Apple Airport Express 6: "mdi:disc-player", # Blu-ray Player 7: "mdi:router-network", # Bridge @@ -87,7 +87,7 @@ DEVICE_ICONS = { 10: "mdi:router-network", # Router 11: "mdi:play-network", # DVR 12: "mdi:gamepad-variant", # Gaming Console - 13: "mdi:desktop-mac", # iMac + 13: "mdi:monitor", # iMac 14: "mdi:tablet", # iPad 15: "mdi:tablet", # iPad Mini 16: "mdi:cellphone", # iPhone 5/5S/5C diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index a4f8a4df14e..ae4186abbb5 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -250,7 +250,7 @@ class NetgearRouter: async with self._api_lock: await self.hass.async_add_executor_job(self._api.reboot) - async def async_check_new_firmware(self) -> None: + async def async_check_new_firmware(self) -> dict[str, Any] | None: """Check for new firmware of the router.""" async with self._api_lock: return await self.hass.async_add_executor_job(self._api.check_new_firmware) diff --git a/homeassistant/components/netgear/translations/hu.json b/homeassistant/components/netgear/translations/hu.json index 4432e08a508..c0c472bad30 100644 --- a/homeassistant/components/netgear/translations/hu.json +++ b/homeassistant/components/netgear/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "config": "Csatlakoz\u00e1si vagy bejelentkez\u00e9si hiba: k\u00e9rj\u00fck, ellen\u0151rizze a konfigur\u00e1ci\u00f3t" + "config": "Csatlakoz\u00e1si vagy bejelentkez\u00e9si hiba: k\u00e9rem, ellen\u0151rizze a konfigur\u00e1ci\u00f3t" }, "step": { "user": { diff --git a/homeassistant/components/netgear/translations/pt.json b/homeassistant/components/netgear/translations/pt.json new file mode 100644 index 00000000000..ece60e5e010 --- /dev/null +++ b/homeassistant/components/netgear/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index b2caf6438bd..0b90023bfd4 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.singleton import singleton @@ -22,7 +22,6 @@ _LOGGER = logging.getLogger(__name__) @singleton(DATA_NETWORK) -@callback async def async_get_network(hass: HomeAssistant) -> Network: """Get network singleton.""" network = Network(hass) @@ -38,8 +37,10 @@ class Network: def __init__(self, hass: HomeAssistant) -> None: """Initialize the Network class.""" - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) - self._data: dict[str, Any] = {} + self._store = Store[dict[str, list[str]]]( + hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) + self._data: dict[str, list[str]] = {} self.adapters: list[Adapter] = [] @property @@ -67,7 +68,7 @@ class Network: async def async_load(self) -> None: """Load config.""" if stored := await self._store.async_load(): - self._data = cast(dict, stored) + self._data = stored async def _async_save(self) -> None: """Save preferences.""" diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 355c17a2ed1..b221f440ff8 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index cc5e6de8641..e381dc95897 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.1"], + "requirements": ["nexia==2.0.2"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py new file mode 100644 index 00000000000..2f68abee847 --- /dev/null +++ b/homeassistant/components/nextdns/__init__.py @@ -0,0 +1,189 @@ +"""The NextDNS component.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging +from typing import TypeVar + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, + ApiError, + InvalidApiKeyError, + NextDns, + Settings, +) +from nextdns.model import NextDnsData + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_DNSSEC, + ATTR_ENCRYPTION, + ATTR_IP_VERSIONS, + ATTR_PROTOCOLS, + ATTR_SETTINGS, + ATTR_STATUS, + CONF_PROFILE_ID, + DOMAIN, + UPDATE_INTERVAL_ANALYTICS, + UPDATE_INTERVAL_SETTINGS, +) + +TCoordinatorData = TypeVar("TCoordinatorData", bound=NextDnsData) + + +class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): + """Class to manage fetching NextDNS data API.""" + + def __init__( + self, + hass: HomeAssistant, + nextdns: NextDns, + profile_id: str, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.nextdns = nextdns + self.profile_id = profile_id + self.profile_name = nextdns.get_profile_name(profile_id) + self.device_info = DeviceInfo( + configuration_url=f"https://my.nextdns.io/{profile_id}/setup", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, str(profile_id))}, + manufacturer="NextDNS Inc.", + name=self.profile_name, + ) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self) -> TCoordinatorData: + """Update data via internal method.""" + try: + async with timeout(10): + return await self._async_update_data_internal() + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + async def _async_update_data_internal(self) -> TCoordinatorData: + """Update data via library.""" + raise NotImplementedError("Update method not implemented") + + +class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsStatus]): + """Class to manage fetching NextDNS analytics status data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsStatus: + """Update data via library.""" + return await self.nextdns.get_analytics_status(self.profile_id) + + +class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsDnssec]): + """Class to manage fetching NextDNS analytics Dnssec data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsDnssec: + """Update data via library.""" + return await self.nextdns.get_analytics_dnssec(self.profile_id) + + +class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsEncryption]): + """Class to manage fetching NextDNS analytics encryption data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsEncryption: + """Update data via library.""" + return await self.nextdns.get_analytics_encryption(self.profile_id) + + +class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsIpVersions]): + """Class to manage fetching NextDNS analytics IP versions data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsIpVersions: + """Update data via library.""" + return await self.nextdns.get_analytics_ip_versions(self.profile_id) + + +class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsProtocols]): + """Class to manage fetching NextDNS analytics protocols data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsProtocols: + """Update data via library.""" + return await self.nextdns.get_analytics_protocols(self.profile_id) + + +class NextDnsSettingsUpdateCoordinator(NextDnsUpdateCoordinator[Settings]): + """Class to manage fetching NextDNS connection data from API.""" + + async def _async_update_data_internal(self) -> Settings: + """Update data via library.""" + return await self.nextdns.get_settings(self.profile_id) + + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +COORDINATORS = [ + (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_IP_VERSIONS, NextDnsIpVersionsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_PROTOCOLS, NextDnsProtocolsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_SETTINGS, NextDnsSettingsUpdateCoordinator, UPDATE_INTERVAL_SETTINGS), + (ATTR_STATUS, NextDnsStatusUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up NextDNS as config entry.""" + api_key = entry.data[CONF_API_KEY] + profile_id = entry.data[CONF_PROFILE_ID] + + websession = async_get_clientsession(hass) + try: + async with timeout(10): + nextdns = await NextDns.create(websession, api_key) + except (ApiError, ClientConnectorError, asyncio.TimeoutError) as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} + + tasks = [] + + # Independent DataUpdateCoordinator is used for each API endpoint to avoid + # unnecessary requests when entities using this endpoint are disabled. + for coordinator_name, coordinator_class, update_interval in COORDINATORS: + hass.data[DOMAIN][entry.entry_id][coordinator_name] = coordinator_class( + hass, nextdns, profile_id, update_interval + ) + tasks.append( + hass.data[DOMAIN][entry.entry_id][ + coordinator_name + ].async_config_entry_first_refresh() + ) + + await asyncio.gather(*tasks) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok: bool = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/nextdns/button.py b/homeassistant/components/nextdns/button.py new file mode 100644 index 00000000000..1f761e6955c --- /dev/null +++ b/homeassistant/components/nextdns/button.py @@ -0,0 +1,55 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsStatusUpdateCoordinator +from .const import ATTR_STATUS, DOMAIN + +PARALLEL_UPDATES = 1 + +CLEAR_LOGS_BUTTON = ButtonEntityDescription( + key="clear_logs", + name="Clear logs", + entity_category=EntityCategory.CONFIG, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add aNextDNS entities from a config_entry.""" + coordinator: NextDnsStatusUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + ATTR_STATUS + ] + + buttons: list[NextDnsButton] = [] + buttons.append(NextDnsButton(coordinator, CLEAR_LOGS_BUTTON)) + + async_add_entities(buttons) + + +class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEntity): + """Define an NextDNS button.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: NextDnsStatusUpdateCoordinator, + description: ButtonEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self.entity_description = description + + async def async_press(self) -> None: + """Trigger cleaning logs.""" + await self.coordinator.nextdns.clear_logs(self.coordinator.profile_id) diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py new file mode 100644 index 00000000000..5c9bf04cfc1 --- /dev/null +++ b/homeassistant/components/nextdns/config_flow.py @@ -0,0 +1,90 @@ +"""Adds config flow for NextDNS.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from nextdns import ApiError, InvalidApiKeyError, NextDns +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_PROFILE_ID, CONF_PROFILE_NAME, DOMAIN + + +class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for NextDNS.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self.nextdns: NextDns | None = None + self.api_key: str | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] = {} + + websession = async_get_clientsession(self.hass) + + if user_input is not None: + self.api_key = user_input[CONF_API_KEY] + try: + async with timeout(10): + self.nextdns = await NextDns.create( + websession, user_input[CONF_API_KEY] + ) + except InvalidApiKeyError: + errors["base"] = "invalid_api_key" + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + else: + return await self.async_step_profiles() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) + + async def async_step_profiles( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the profiles step.""" + errors: dict[str, str] = {} + + assert self.nextdns is not None + + if user_input is not None: + profile_name = user_input[CONF_PROFILE_NAME] + profile_id = self.nextdns.get_profile_id(profile_name) + + await self.async_set_unique_id(profile_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=profile_name, + data={CONF_PROFILE_ID: profile_id, CONF_API_KEY: self.api_key}, + ) + + return self.async_show_form( + step_id="profiles", + data_schema=vol.Schema( + { + vol.Required(CONF_PROFILE_NAME): vol.In( + [profile.name for profile in self.nextdns.profiles] + ) + } + ), + errors=errors, + ) diff --git a/homeassistant/components/nextdns/const.py b/homeassistant/components/nextdns/const.py new file mode 100644 index 00000000000..d455dd79635 --- /dev/null +++ b/homeassistant/components/nextdns/const.py @@ -0,0 +1,17 @@ +"""Constants for NextDNS integration.""" +from datetime import timedelta + +ATTR_DNSSEC = "dnssec" +ATTR_ENCRYPTION = "encryption" +ATTR_IP_VERSIONS = "ip_versions" +ATTR_PROTOCOLS = "protocols" +ATTR_SETTINGS = "settings" +ATTR_STATUS = "status" + +CONF_PROFILE_ID = "profile_id" +CONF_PROFILE_NAME = "profile_name" + +UPDATE_INTERVAL_ANALYTICS = timedelta(minutes=10) +UPDATE_INTERVAL_SETTINGS = timedelta(minutes=1) + +DOMAIN = "nextdns" diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py new file mode 100644 index 00000000000..4be22684395 --- /dev/null +++ b/homeassistant/components/nextdns/diagnostics.py @@ -0,0 +1,45 @@ +"""Diagnostics support for NextDNS.""" +from __future__ import annotations + +from dataclasses import asdict + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant + +from .const import ( + ATTR_DNSSEC, + ATTR_ENCRYPTION, + ATTR_IP_VERSIONS, + ATTR_PROTOCOLS, + ATTR_STATUS, + CONF_PROFILE_ID, + DOMAIN, +) + +TO_REDACT = {CONF_API_KEY, CONF_PROFILE_ID, CONF_UNIQUE_ID} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + coordinators = hass.data[DOMAIN][config_entry.entry_id] + + dnssec_coordinator = coordinators[ATTR_DNSSEC] + encryption_coordinator = coordinators[ATTR_ENCRYPTION] + ip_versions_coordinator = coordinators[ATTR_IP_VERSIONS] + protocols_coordinator = coordinators[ATTR_PROTOCOLS] + status_coordinator = coordinators[ATTR_STATUS] + + diagnostics_data = { + "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), + "dnssec_coordinator_data": asdict(dnssec_coordinator.data), + "encryption_coordinator_data": asdict(encryption_coordinator.data), + "ip_versions_coordinator_data": asdict(ip_versions_coordinator.data), + "protocols_coordinator_data": asdict(protocols_coordinator.data), + "status_coordinator_data": asdict(status_coordinator.data), + } + + return diagnostics_data diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json new file mode 100644 index 00000000000..a427f930db8 --- /dev/null +++ b/homeassistant/components/nextdns/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "nextdns", + "name": "NextDNS", + "documentation": "https://www.home-assistant.io/integrations/nextdns", + "codeowners": ["@bieniu"], + "requirements": ["nextdns==1.0.1"], + "config_flow": true, + "iot_class": "cloud_polling", + "loggers": ["nextdns"] +} diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py new file mode 100644 index 00000000000..168c0be8cd0 --- /dev/null +++ b/homeassistant/components/nextdns/sensor.py @@ -0,0 +1,351 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic + +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, +) + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsUpdateCoordinator, TCoordinatorData +from .const import ( + ATTR_DNSSEC, + ATTR_ENCRYPTION, + ATTR_IP_VERSIONS, + ATTR_PROTOCOLS, + ATTR_STATUS, + DOMAIN, +) + +PARALLEL_UPDATES = 1 + + +@dataclass +class NextDnsSensorRequiredKeysMixin(Generic[TCoordinatorData]): + """Class for NextDNS entity required keys.""" + + coordinator_type: str + value: Callable[[TCoordinatorData], StateType] + + +@dataclass +class NextDnsSensorEntityDescription( + SensorEntityDescription, + NextDnsSensorRequiredKeysMixin[TCoordinatorData], +): + """NextDNS sensor entity description.""" + + +SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( + NextDnsSensorEntityDescription[AnalyticsStatus]( + key="all_queries", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="DNS queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.all_queries, + ), + NextDnsSensorEntityDescription[AnalyticsStatus]( + key="blocked_queries", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="DNS queries blocked", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.blocked_queries, + ), + NextDnsSensorEntityDescription[AnalyticsStatus]( + key="relayed_queries", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="DNS queries relayed", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.relayed_queries, + ), + NextDnsSensorEntityDescription[AnalyticsStatus]( + key="blocked_queries_ratio", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="DNS queries blocked ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.blocked_queries_ratio, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="doh_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="DNS-over-HTTPS queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.doh_queries, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="dot_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="DNS-over-TLS queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.dot_queries, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="doq_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="DNS-over-QUIC queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.doq_queries, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="tcp_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="TCP Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.tcp_queries, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="udp_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="UDP queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.udp_queries, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="doh_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_registry_enabled_default=False, + icon="mdi:dns", + entity_category=EntityCategory.DIAGNOSTIC, + name="DNS-over-HTTPS queries ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doh_queries_ratio, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="dot_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="DNS-over-TLS queries ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.dot_queries_ratio, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="doq_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_registry_enabled_default=False, + icon="mdi:dns", + entity_category=EntityCategory.DIAGNOSTIC, + name="DNS-over-QUIC queries ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doq_queries_ratio, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="tcp_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="TCP Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.tcp_queries_ratio, + ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="udp_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="UDP queries ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.udp_queries_ratio, + ), + NextDnsSensorEntityDescription[AnalyticsEncryption]( + key="encrypted_queries", + coordinator_type=ATTR_ENCRYPTION, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock", + name="Encrypted queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.encrypted_queries, + ), + NextDnsSensorEntityDescription[AnalyticsEncryption]( + key="unencrypted_queries", + coordinator_type=ATTR_ENCRYPTION, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-open", + name="Unencrypted queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.unencrypted_queries, + ), + NextDnsSensorEntityDescription[AnalyticsEncryption]( + key="encrypted_queries_ratio", + coordinator_type=ATTR_ENCRYPTION, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock", + name="Encrypted queries ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.encrypted_queries_ratio, + ), + NextDnsSensorEntityDescription[AnalyticsIpVersions]( + key="ipv4_queries", + coordinator_type=ATTR_IP_VERSIONS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:ip", + name="IPv4 queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.ipv4_queries, + ), + NextDnsSensorEntityDescription[AnalyticsIpVersions]( + key="ipv6_queries", + coordinator_type=ATTR_IP_VERSIONS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:ip", + name="IPv6 queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.ipv6_queries, + ), + NextDnsSensorEntityDescription[AnalyticsIpVersions]( + key="ipv6_queries_ratio", + coordinator_type=ATTR_IP_VERSIONS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:ip", + name="IPv6 queries ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.ipv6_queries_ratio, + ), + NextDnsSensorEntityDescription[AnalyticsDnssec]( + key="validated_queries", + coordinator_type=ATTR_DNSSEC, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-check", + name="DNSSEC validated queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.validated_queries, + ), + NextDnsSensorEntityDescription[AnalyticsDnssec]( + key="not_validated_queries", + coordinator_type=ATTR_DNSSEC, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-alert", + name="DNSSEC not validated queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.not_validated_queries, + ), + NextDnsSensorEntityDescription[AnalyticsDnssec]( + key="validated_queries_ratio", + coordinator_type=ATTR_DNSSEC, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-check", + name="DNSSEC validated queries ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.validated_queries_ratio, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add a NextDNS entities from a config_entry.""" + sensors: list[NextDnsSensor] = [] + coordinators = hass.data[DOMAIN][entry.entry_id] + + for description in SENSORS: + sensors.append( + NextDnsSensor(coordinators[description.coordinator_type], description) + ) + + async_add_entities(sensors) + + +class NextDnsSensor( + CoordinatorEntity[NextDnsUpdateCoordinator[TCoordinatorData]], SensorEntity +): + """Define an NextDNS sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: NextDnsUpdateCoordinator[TCoordinatorData], + description: NextDnsSensorEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_native_value = description.value(coordinator.data) + self.entity_description: NextDnsSensorEntityDescription = description + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = self.entity_description.value(self.coordinator.data) + self.async_write_ha_state() diff --git a/homeassistant/components/nextdns/strings.json b/homeassistant/components/nextdns/strings.json new file mode 100644 index 00000000000..59319881c02 --- /dev/null +++ b/homeassistant/components/nextdns/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "profiles": { + "data": { + "profile": "Profile" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "This NextDNS profile is already configured." + } + }, + "system_health": { + "info": { + "can_reach_server": "Reach server" + } + } +} diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py new file mode 100644 index 00000000000..4bd3c14c20f --- /dev/null +++ b/homeassistant/components/nextdns/switch.py @@ -0,0 +1,247 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Generic + +from nextdns import Settings + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsSettingsUpdateCoordinator, TCoordinatorData +from .const import ATTR_SETTINGS, DOMAIN + +PARALLEL_UPDATES = 1 + + +@dataclass +class NextDnsSwitchRequiredKeysMixin(Generic[TCoordinatorData]): + """Class for NextDNS entity required keys.""" + + state: Callable[[TCoordinatorData], bool] + + +@dataclass +class NextDnsSwitchEntityDescription( + SwitchEntityDescription, NextDnsSwitchRequiredKeysMixin[TCoordinatorData] +): + """NextDNS switch entity description.""" + + +SWITCHES = ( + NextDnsSwitchEntityDescription[Settings]( + key="block_page", + name="Block page", + entity_category=EntityCategory.CONFIG, + icon="mdi:web-cancel", + state=lambda data: data.block_page, + ), + NextDnsSwitchEntityDescription[Settings]( + key="cache_boost", + name="Cache boost", + entity_category=EntityCategory.CONFIG, + icon="mdi:memory", + state=lambda data: data.cache_boost, + ), + NextDnsSwitchEntityDescription[Settings]( + key="cname_flattening", + name="CNAME flattening", + entity_category=EntityCategory.CONFIG, + icon="mdi:tournament", + state=lambda data: data.cname_flattening, + ), + NextDnsSwitchEntityDescription[Settings]( + key="anonymized_ecs", + name="Anonymized EDNS client subnet", + entity_category=EntityCategory.CONFIG, + icon="mdi:incognito", + state=lambda data: data.anonymized_ecs, + ), + NextDnsSwitchEntityDescription[Settings]( + key="logs", + name="Logs", + entity_category=EntityCategory.CONFIG, + icon="mdi:file-document-outline", + state=lambda data: data.logs, + ), + NextDnsSwitchEntityDescription[Settings]( + key="web3", + name="Web3", + entity_category=EntityCategory.CONFIG, + icon="mdi:web", + state=lambda data: data.web3, + ), + NextDnsSwitchEntityDescription[Settings]( + key="allow_affiliate", + name="Allow affiliate & tracking links", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.allow_affiliate, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_disguised_trackers", + name="Block disguised third-party trackers", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_disguised_trackers, + ), + NextDnsSwitchEntityDescription[Settings]( + key="ai_threat_detection", + name="AI-Driven threat detection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.ai_threat_detection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_csam", + name="Block child sexual abuse material", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_csam, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_ddns", + name="Block dynamic DNS hostnames", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_ddns, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_nrd", + name="Block newly registered domains", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_nrd, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_parked_domains", + name="Block parked domains", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_parked_domains, + ), + NextDnsSwitchEntityDescription[Settings]( + key="cryptojacking_protection", + name="Cryptojacking protection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.cryptojacking_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="dga_protection", + name="Domain generation algorithms protection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.dga_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="dns_rebinding_protection", + name="DNS rebinding protection", + entity_category=EntityCategory.CONFIG, + icon="mdi:dns", + state=lambda data: data.dns_rebinding_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="google_safe_browsing", + name="Google safe browsing", + entity_category=EntityCategory.CONFIG, + icon="mdi:google", + state=lambda data: data.google_safe_browsing, + ), + NextDnsSwitchEntityDescription[Settings]( + key="idn_homograph_attacks_protection", + name="IDN homograph attacks protection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.idn_homograph_attacks_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="threat_intelligence_feeds", + name="Threat intelligence feeds", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.threat_intelligence_feeds, + ), + NextDnsSwitchEntityDescription[Settings]( + key="typosquatting_protection", + name="Typosquatting protection", + entity_category=EntityCategory.CONFIG, + icon="mdi:keyboard-outline", + state=lambda data: data.typosquatting_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_bypass_methods", + name="Block bypass methods", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_bypass_methods, + ), + NextDnsSwitchEntityDescription[Settings]( + key="safesearch", + name="Force SafeSearch", + entity_category=EntityCategory.CONFIG, + icon="mdi:search-web", + state=lambda data: data.safesearch, + ), + NextDnsSwitchEntityDescription[Settings]( + key="youtube_restricted_mode", + name="Force YouTube restricted mode", + entity_category=EntityCategory.CONFIG, + icon="mdi:youtube", + state=lambda data: data.youtube_restricted_mode, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add NextDNS entities from a config_entry.""" + coordinator: NextDnsSettingsUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + ATTR_SETTINGS + ] + + switches: list[NextDnsSwitch] = [] + for description in SWITCHES: + switches.append(NextDnsSwitch(coordinator, description)) + + async_add_entities(switches) + + +class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchEntity): + """Define an NextDNS switch.""" + + _attr_has_entity_name = True + entity_description: NextDnsSwitchEntityDescription + + def __init__( + self, + coordinator: NextDnsSettingsUpdateCoordinator, + description: NextDnsSwitchEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_is_on = description.state(coordinator.data) + self.entity_description = description + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = self.entity_description.state(self.coordinator.data) + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on switch.""" + result = await self.coordinator.nextdns.set_setting( + self.coordinator.profile_id, self.entity_description.key, True + ) + + if result: + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off switch.""" + result = await self.coordinator.nextdns.set_setting( + self.coordinator.profile_id, self.entity_description.key, False + ) + + if result: + self._attr_is_on = False + self.async_write_ha_state() diff --git a/homeassistant/components/nextdns/system_health.py b/homeassistant/components/nextdns/system_health.py new file mode 100644 index 00000000000..a56a89914b8 --- /dev/null +++ b/homeassistant/components/nextdns/system_health.py @@ -0,0 +1,24 @@ +"""Provide info to system health.""" +from __future__ import annotations + +from typing import Any + +from nextdns.const import API_ENDPOINT + +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: + """Get info for the info page.""" + return { + "can_reach_server": system_health.async_check_can_reach_url(hass, API_ENDPOINT) + } diff --git a/homeassistant/components/nextdns/translations/ar.json b/homeassistant/components/nextdns/translations/ar.json new file mode 100644 index 00000000000..965e46a92e7 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ar.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644", + "unknown": "\u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" + }, + "step": { + "profiles": { + "data": { + "profile": "\u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u062c\u0627\u0631\u064a \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u062e\u0627\u062f\u0645" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ca.json b/homeassistant/components/nextdns/translations/ca.json new file mode 100644 index 00000000000..dd38c4f0b91 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest perfil NextDNS ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "profiles": { + "data": { + "profile": "Perfil" + } + }, + "user": { + "data": { + "api_key": "Clau API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor accessible" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/de.json b/homeassistant/components/nextdns/translations/de.json new file mode 100644 index 00000000000..51a5eaf5edd --- /dev/null +++ b/homeassistant/components/nextdns/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses NextDNS-Profil ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Server erreichen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/el.json b/homeassistant/components/nextdns/translations/el.json new file mode 100644 index 00000000000..14c913febcf --- /dev/null +++ b/homeassistant/components/nextdns/translations/el.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb NextDNS \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "profiles": { + "data": { + "profile": "\u03a0\u03c1\u03bf\u03c6\u03af\u03bb" + } + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u03a0\u03c1\u03bf\u03c3\u03ad\u03b3\u03b3\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/en.json b/homeassistant/components/nextdns/translations/en.json new file mode 100644 index 00000000000..d5634514010 --- /dev/null +++ b/homeassistant/components/nextdns/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "This NextDNS profile is already configured." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "unknown": "Unexpected error" + }, + "step": { + "profiles": { + "data": { + "profile": "Profile" + } + }, + "user": { + "data": { + "api_key": "API Key" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Reach server" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/et.json b/homeassistant/components/nextdns/translations/et.json new file mode 100644 index 00000000000..eaf8efc0341 --- /dev/null +++ b/homeassistant/components/nextdns/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "NextDNS-profiil on juba seadistatud." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Vigane API v\u00f5ti", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "profiles": { + "data": { + "profile": "Profiil" + } + }, + "user": { + "data": { + "api_key": "API v\u00f5ti" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u00dchendu serveriga" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/fr.json b/homeassistant/components/nextdns/translations/fr.json new file mode 100644 index 00000000000..c5f91ef80cc --- /dev/null +++ b/homeassistant/components/nextdns/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ce profil NextDNS est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 d'API non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Serveur atteint" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/hu.json b/homeassistant/components/nextdns/translations/hu.json new file mode 100644 index 00000000000..2491f625b4f --- /dev/null +++ b/homeassistant/components/nextdns/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ez a NextDNS profil m\u00e1r be van \u00e1ll\u00edtva." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API kulcs" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Szerver el\u00e9r\u00e9se" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/id.json b/homeassistant/components/nextdns/translations/id.json new file mode 100644 index 00000000000..39f87faf813 --- /dev/null +++ b/homeassistant/components/nextdns/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Profil NextDNS ini sudah dikonfigurasi." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "Kunci API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/it.json b/homeassistant/components/nextdns/translations/it.json new file mode 100644 index 00000000000..68d98f77d09 --- /dev/null +++ b/homeassistant/components/nextdns/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Questo profilo NextDNS \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "profiles": { + "data": { + "profile": "Profilo" + } + }, + "user": { + "data": { + "api_key": "Chiave API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Server raggiungibile" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ja.json b/homeassistant/components/nextdns/translations/ja.json new file mode 100644 index 00000000000..df57113cc58 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ja.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u3053\u306eNextDNS\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "profiles": { + "data": { + "profile": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb" + } + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/nl.json b/homeassistant/components/nextdns/translations/nl.json new file mode 100644 index 00000000000..436e1a68b7d --- /dev/null +++ b/homeassistant/components/nextdns/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel", + "unknown": "Onverwachte fout" + }, + "step": { + "profiles": { + "data": { + "profile": "Profiel" + } + }, + "user": { + "data": { + "api_key": "API-sleutel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/no.json b/homeassistant/components/nextdns/translations/no.json new file mode 100644 index 00000000000..fb4d6616587 --- /dev/null +++ b/homeassistant/components/nextdns/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "unknown": "Totalt uventet feil" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pl.json b/homeassistant/components/nextdns/translations/pl.json new file mode 100644 index 00000000000..f23d4da29d3 --- /dev/null +++ b/homeassistant/components/nextdns/translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ten profil NextDNS jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "Klucz API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt-BR.json b/homeassistant/components/nextdns/translations/pt-BR.json new file mode 100644 index 00000000000..90d7cf3f31e --- /dev/null +++ b/homeassistant/components/nextdns/translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Este perfil NextDNS j\u00e1 est\u00e1 configurado." + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "profiles": { + "data": { + "profile": "Perfil" + } + }, + "user": { + "data": { + "api_key": "Chave de API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor de alcance" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt.json b/homeassistant/components/nextdns/translations/pt.json new file mode 100644 index 00000000000..cd774c0c6cd --- /dev/null +++ b/homeassistant/components/nextdns/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ru.json b/homeassistant/components/nextdns/translations/ru.json new file mode 100644 index 00000000000..952058cf5f6 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e\u0442 \u043f\u0440\u043e\u0444\u0438\u043b\u044c NextDNS \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "profiles": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" + } + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/tr.json b/homeassistant/components/nextdns/translations/tr.json new file mode 100644 index 00000000000..5cf9dc9b2c2 --- /dev/null +++ b/homeassistant/components/nextdns/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Bu NextDNS profili zaten yap\u0131land\u0131r\u0131lm\u0131\u015f." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Eri\u015fim sunucusu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/zh-Hant.json b/homeassistant/components/nextdns/translations/zh-Hant.json new file mode 100644 index 00000000000..5544f29662f --- /dev/null +++ b/homeassistant/components/nextdns/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "NextDNS \u500b\u4eba\u8a2d\u5b9a\u5df2\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "profiles": { + "data": { + "profile": "\u500b\u4eba\u8a2d\u5b9a" + } + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Reach \u4f3a\u670d\u5668" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/hu.json b/homeassistant/components/nfandroidtv/translations/hu.json index 65cd2bebc01..6e83a5f7c6d 100644 --- a/homeassistant/components/nfandroidtv/translations/hu.json +++ b/homeassistant/components/nfandroidtv/translations/hu.json @@ -13,7 +13,7 @@ "host": "C\u00edm", "name": "Elnevez\u00e9s" }, - "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." + "description": "K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." } } } diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index fff28117234..3db6768efcb 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -13,7 +13,7 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002" + "description": "\u3053\u306e\u7d71\u5408\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/nfandroidtv/translations/pt.json b/homeassistant/components/nfandroidtv/translations/pt.json new file mode 100644 index 00000000000..e3233d5779f --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 8e0c6c2b791..88f12ffa4bc 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_type=dr.DeviceEntryType.SERVICE, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index dd60afe2ff6..400c9f0ab8e 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -121,21 +121,22 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the NILU air quality sensor.""" - name = config.get(CONF_NAME) - area = config.get(CONF_AREA) - stations = config.get(CONF_STATION) - show_on_map = config.get(CONF_SHOW_ON_MAP) + name: str = config[CONF_NAME] + area: str | None = config.get(CONF_AREA) + stations: list[str] | None = config.get(CONF_STATION) + show_on_map: bool = config[CONF_SHOW_ON_MAP] sensors = [] if area: stations = lookup_stations_in_area(area) - elif not area and not stations: + elif not stations: latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) location_client = create_location_client(latitude, longitude) stations = location_client.station_names + assert stations is not None for station in stations: client = NiluData(create_station_client(station)) client.update() @@ -195,61 +196,61 @@ class NiluSensor(AirQualityEntity): return self._name @property - def air_quality_index(self) -> str: + def air_quality_index(self) -> str | None: """Return the Air Quality Index (AQI).""" return self._max_aqi @property - def carbon_monoxide(self) -> str: + def carbon_monoxide(self) -> str | None: """Return the CO (carbon monoxide) level.""" return self.get_component_state(CO) @property - def carbon_dioxide(self) -> str: + def carbon_dioxide(self) -> str | None: """Return the CO2 (carbon dioxide) level.""" return self.get_component_state(CO2) @property - def nitrogen_oxide(self) -> str: + def nitrogen_oxide(self) -> str | None: """Return the N2O (nitrogen oxide) level.""" return self.get_component_state(NOX) @property - def nitrogen_monoxide(self) -> str: + def nitrogen_monoxide(self) -> str | None: """Return the NO (nitrogen monoxide) level.""" return self.get_component_state(NO) @property - def nitrogen_dioxide(self) -> str: + def nitrogen_dioxide(self) -> str | None: """Return the NO2 (nitrogen dioxide) level.""" return self.get_component_state(NO2) @property - def ozone(self) -> str: + def ozone(self) -> str | None: """Return the O3 (ozone) level.""" return self.get_component_state(OZONE) @property - def particulate_matter_2_5(self) -> str: + def particulate_matter_2_5(self) -> str | None: """Return the particulate matter 2.5 level.""" return self.get_component_state(PM25) @property - def particulate_matter_10(self) -> str: + def particulate_matter_10(self) -> str | None: """Return the particulate matter 10 level.""" return self.get_component_state(PM10) @property - def particulate_matter_0_1(self) -> str: + def particulate_matter_0_1(self) -> str | None: """Return the particulate matter 0.1 level.""" return self.get_component_state(PM1) @property - def sulphur_dioxide(self) -> str: + def sulphur_dioxide(self) -> str | None: """Return the SO2 (sulphur dioxide) level.""" return self.get_component_state(SO2) - def get_component_state(self, component_name: str) -> str: + def get_component_state(self, component_name: str) -> str | None: """Return formatted value of specified component.""" if component_name in self._api.data.sensors: sensor = self._api.data.sensors[component_name] diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index c5375c96785..17e0280ca50 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nina/translations/ar.json b/homeassistant/components/nina/translations/ar.json new file mode 100644 index 00000000000..d0c2b4eccbc --- /dev/null +++ b/homeassistant/components/nina/translations/ar.json @@ -0,0 +1,24 @@ +{ + "options": { + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644", + "no_selection": "\u0645\u0646 \u0641\u0636\u0644\u0643 \u0627\u062e\u062a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0623\u0642\u0644 \u0645\u062f\u064a\u0646\u0629/\u0645\u0642\u0627\u0637\u0639\u0629 \u0648\u0627\u062d\u062f\u0629", + "unknown": "\u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629/\u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (A-D)", + "_e_to_h": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629/\u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (E-H)", + "_i_to_l": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629/\u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (I-L)", + "_m_to_q": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629 / \u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (MQ)", + "_r_to_u": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629 / \u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (RU)", + "_v_to_z": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629 / \u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (VZ)", + "corona_filter": "\u0623\u0632\u0644 \u062a\u062d\u0630\u064a\u0631\u0627\u062a \u0643\u0648\u0631\u0648\u0646\u0627", + "slots": "\u0627\u0644\u062a\u062d\u0630\u064a\u0631\u0627\u062a \u0627\u0644\u0642\u0635\u0648\u0649 \u0644\u0643\u0644 \u0645\u062f\u064a\u0646\u0629/\u0645\u062d\u0627\u0641\u0638\u0629" + }, + "title": "\u0627\u0644\u062e\u064a\u0627\u0631\u0627\u062a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ca.json b/homeassistant/components/nina/translations/ca.json index 41d274d779d..1ff79090d3c 100644 --- a/homeassistant/components/nina/translations/ca.json +++ b/homeassistant/components/nina/translations/ca.json @@ -23,5 +23,27 @@ "title": "Selecciona ciutat/comtat" } } + }, + "options": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_selection": "Seleccioneu almenys una ciutat/comtat", + "unknown": "Error inesperat" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Ciutat/comtat (A-D)", + "_e_to_h": "Ciutat/comtat (E-H)", + "_i_to_l": "Ciutat/comtat (I-L)", + "_m_to_q": "Ciutat/comtat (M-Q)", + "_r_to_u": "Ciutat/comtat (R-U)", + "_v_to_z": "Ciutat/comtat (V-Z)", + "corona_filter": "Elimina els avisos de corona", + "slots": "Nombre d'avisos m\u00e0xims per ciutat/comtat" + }, + "title": "Opcions" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/de.json b/homeassistant/components/nina/translations/de.json index 1e7e1a3e70e..4e6b881b051 100644 --- a/homeassistant/components/nina/translations/de.json +++ b/homeassistant/components/nina/translations/de.json @@ -23,5 +23,27 @@ "title": "Stadt/Landkreis ausw\u00e4hlen" } } + }, + "options": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "no_selection": "Bitte w\u00e4hle mindestens eine Stadt/einen Landkreis aus", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Stadt/Landkreis (A-D)", + "_e_to_h": "Stadt/Landkreis (E-H)", + "_i_to_l": "Stadt/Landkreis (I-L)", + "_m_to_q": "Stadt/Landkreis (M-Q)", + "_r_to_u": "Stadt/Landkreis (R-U)", + "_v_to_z": "Stadt/Landkreis (V-Z)", + "corona_filter": "Corona-Warnungen entfernen", + "slots": "Maximale Warnungen pro Stadt/Landkreis" + }, + "title": "Optionen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/el.json b/homeassistant/components/nina/translations/el.json index 9faf567662a..8466a9e5215 100644 --- a/homeassistant/components/nina/translations/el.json +++ b/homeassistant/components/nina/translations/el.json @@ -23,5 +23,27 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03cc\u03bb\u03b7\u03c2/\u03bd\u03bf\u03bc\u03bf\u03cd" } } + }, + "options": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_selection": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03af\u03b1 \u03c0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (\u0391-D)", + "_e_to_h": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (\u0395-\u0397)", + "_i_to_l": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (I-L)", + "_m_to_q": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (\u039c-Q)", + "_r_to_u": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (R-U)", + "_v_to_z": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (V-Z)", + "corona_filter": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd Corona", + "slots": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b5\u03c2 \u03c0\u03c1\u03bf\u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b1\u03bd\u03ac \u03c0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/et.json b/homeassistant/components/nina/translations/et.json index db454b7996c..4eb59ab43ae 100644 --- a/homeassistant/components/nina/translations/et.json +++ b/homeassistant/components/nina/translations/et.json @@ -23,5 +23,27 @@ "title": "Vali linn/maakond" } } + }, + "options": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "no_selection": "Vali v\u00e4hemalt \u00fcks linn/maakond", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Linn/maakond (A-D)", + "_e_to_h": "Linn/maakond (E-H)", + "_i_to_l": "Linn/maakond (I-L)", + "_m_to_q": "Linn/maakond (M-Q)", + "_r_to_u": "Linn/maakond (R-U)", + "_v_to_z": "Linn/maakond (V-Z)", + "corona_filter": "Eemalda koroonahoiatused", + "slots": "Maksimaalne hoiatuste arv linna/maakonna kohta" + }, + "title": "Valikud" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/fr.json b/homeassistant/components/nina/translations/fr.json index 83f994b79a4..7d76b2aa5db 100644 --- a/homeassistant/components/nina/translations/fr.json +++ b/homeassistant/components/nina/translations/fr.json @@ -5,22 +5,44 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "no_selection": "Veuillez s\u00e9lectionner au moins une ville/une region", + "no_selection": "Veuillez s\u00e9lectionner au moins une ville ou un comt\u00e9", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { - "_a_to_d": "Ville/R\u00e9gion (A-D)", - "_e_to_h": "Ville/R\u00e9gion (E-H)", - "_i_to_l": "Ville/R\u00e9gion (I-L)", - "_m_to_q": "Ville/R\u00e9gion (M-Q)", - "_r_to_u": "Ville/R\u00e9gion (R-U)", - "_v_to_z": "Ville/R\u00e9gion (V-Z)", + "_a_to_d": "Ville / comt\u00e9 (A-D)", + "_e_to_h": "Ville / comt\u00e9 (E-H)", + "_i_to_l": "Ville / comt\u00e9 (I-L)", + "_m_to_q": "Ville / comt\u00e9 (M-Q)", + "_r_to_u": "Ville / comt\u00e9 (R-U)", + "_v_to_z": "Ville / comt\u00e9 (V-Z)", "corona_filter": "Supprimer les avertissements Corona", - "slots": "Nombre maximal d'avertissements par ville/region" + "slots": "Nombre maximal d'avertissements par ville ou comt\u00e9" }, - "title": "S\u00e9lectionnez la ville/la region" + "title": "S\u00e9lectionnez la ville ou le comt\u00e9" + } + } + }, + "options": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "no_selection": "Veuillez s\u00e9lectionner au moins une ville ou un comt\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Ville / comt\u00e9 (A-D)", + "_e_to_h": "Ville / comt\u00e9 (E-H)", + "_i_to_l": "Ville / comt\u00e9 (I-L)", + "_m_to_q": "Ville / comt\u00e9 (M-Q)", + "_r_to_u": "Ville / comt\u00e9 (R-U)", + "_v_to_z": "Ville / comt\u00e9 (V-Z)", + "corona_filter": "Supprimer les avertissements Corona", + "slots": "Nombre maximal d'avertissements par ville ou comt\u00e9" + }, + "title": "Options" } } } diff --git a/homeassistant/components/nina/translations/hu.json b/homeassistant/components/nina/translations/hu.json index 24a0d59cbae..73c76f45124 100644 --- a/homeassistant/components/nina/translations/hu.json +++ b/homeassistant/components/nina/translations/hu.json @@ -23,5 +23,27 @@ "title": "V\u00e1lasszon v\u00e1rost/megy\u00e9t" } } + }, + "options": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_selection": "K\u00e9rem, v\u00e1lasszon legal\u00e1bb egy v\u00e1rost/megy\u00e9t", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "init": { + "data": { + "_a_to_d": "V\u00e1ros/megye (A-D)", + "_e_to_h": "V\u00e1ros/megye (E-H)", + "_i_to_l": "V\u00e1ros/megye (I-L)", + "_m_to_q": "V\u00e1ros/megye (M-Q)", + "_r_to_u": "V\u00e1ros/megye (R-U)", + "_v_to_z": "V\u00e1ros/megye (V-Z)", + "corona_filter": "Corona figyelmeztet\u00e9sek bez\u00e1r\u00e1sa", + "slots": "A figyelmeztet\u00e9sek maxim\u00e1lis sz\u00e1ma v\u00e1rosonk\u00e9nt/megy\u00e9nk\u00e9nt" + }, + "title": "Be\u00e1ll\u00edt\u00e1sok" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/id.json b/homeassistant/components/nina/translations/id.json index 03f6feecc22..f827ef935f7 100644 --- a/homeassistant/components/nina/translations/id.json +++ b/homeassistant/components/nina/translations/id.json @@ -23,5 +23,27 @@ "title": "Pilih kota/kabupaten" } } + }, + "options": { + "error": { + "cannot_connect": "Gagal terhubung", + "no_selection": "Pilih setidaknya satu kota/kabupaten", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Kota/kabupaten (A-D)", + "_e_to_h": "Kota/kabupaten (E-H)", + "_i_to_l": "Kota/kabupaten (I-L)", + "_m_to_q": "Kota/kabupaten (M-Q)", + "_r_to_u": "Kota/kabupaten (R-U)", + "_v_to_z": "Kota/kabupaten (V-Z)", + "corona_filter": "Hapus Peringatan Corona", + "slots": "Peringatan maksimum per kota/kabupaten" + }, + "title": "Opsi" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/it.json b/homeassistant/components/nina/translations/it.json index de54f8f9c1d..0e329cef6bd 100644 --- a/homeassistant/components/nina/translations/it.json +++ b/homeassistant/components/nina/translations/it.json @@ -23,5 +23,27 @@ "title": "Seleziona citt\u00e0/provincia" } } + }, + "options": { + "error": { + "cannot_connect": "Impossibile connettersi", + "no_selection": "Seleziona almeno una citt\u00e0/provincia", + "unknown": "Errore imprevisto" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Citt\u00e0/provincia (A-D)", + "_e_to_h": "Citt\u00e0/provincia (E-H)", + "_i_to_l": "Citt\u00e0/provincia (I-L)", + "_m_to_q": "Citt\u00e0/provincia (M-Q)", + "_r_to_u": "Citt\u00e0/provincia (R-U)", + "_v_to_z": "Citt\u00e0/provincia (V-Z)", + "corona_filter": "Rimuovi avvisi Corona", + "slots": "Avvisi massimi per citt\u00e0/provincia" + }, + "title": "Opzioni" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ja.json b/homeassistant/components/nina/translations/ja.json index 7c765025ae8..5591b2b0986 100644 --- a/homeassistant/components/nina/translations/ja.json +++ b/homeassistant/components/nina/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -23,5 +23,27 @@ "title": "\u5e02\u533a\u753a\u6751/\u7fa4\u3092\u9078\u629e" } } + }, + "options": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "no_selection": "\u5c11\u306a\u304f\u3068\u30821\u3064\u306e\u5e02\u533a\u753a\u6751/\u90e1\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "init": { + "data": { + "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "City/county (M-Q)", + "_r_to_u": "City/county (R-U)", + "_v_to_z": "City/county (V-Z)", + "corona_filter": "\u30b3\u30ed\u30ca\u8b66\u544a\u306e\u524a\u9664", + "slots": "1\u5e02\u533a\u753a\u6751/\u90e1\u3042\u305f\u308a\u306e\u6700\u5927\u8b66\u544a\u6570" + }, + "title": "\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/nl.json b/homeassistant/components/nina/translations/nl.json index 4b0100f9f07..7c18eb1e6b6 100644 --- a/homeassistant/components/nina/translations/nl.json +++ b/homeassistant/components/nina/translations/nl.json @@ -23,5 +23,22 @@ "title": "Selecteer stad/provincie" } } + }, + "options": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "init": { + "data": { + "_i_to_l": "Stad/provincie (I-L)", + "_m_to_q": "Stad/provincie (M-Q)", + "_r_to_u": "Stad/provincie (R-U)", + "_v_to_z": "Stad/provincie (V-Z)" + }, + "title": "Opties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/no.json b/homeassistant/components/nina/translations/no.json index a4e062a7812..6a13c698941 100644 --- a/homeassistant/components/nina/translations/no.json +++ b/homeassistant/components/nina/translations/no.json @@ -23,5 +23,27 @@ "title": "Velg by/fylke" } } + }, + "options": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "no_selection": "Velg minst \u00e9n by/fylke", + "unknown": "Uventet feil" + }, + "step": { + "init": { + "data": { + "_a_to_d": "By/fylke (AD)", + "_e_to_h": "By/fylke (EH)", + "_i_to_l": "By/fylke (IL)", + "_m_to_q": "By/fylke (MQ)", + "_r_to_u": "By/fylke (RU)", + "_v_to_z": "By/fylke (VZ)", + "corona_filter": "Fjern koronaadvarsler", + "slots": "Maksimal advarsler per by/fylke" + }, + "title": "Alternativer" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pl.json b/homeassistant/components/nina/translations/pl.json index 631127679c1..60c97a1c1c0 100644 --- a/homeassistant/components/nina/translations/pl.json +++ b/homeassistant/components/nina/translations/pl.json @@ -23,5 +23,27 @@ "title": "Wybierz miasto/powiat" } } + }, + "options": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_selection": "Prosz\u0119 wybra\u0107 przynajmniej jedno miasto lub powiat", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Miasto/powiat (A-D)", + "_e_to_h": "Miasto/powiat (E-H)", + "_i_to_l": "Miasto/powiat (I-L)", + "_m_to_q": "Miasto/powiat (M-Q)", + "_r_to_u": "Miasto/powiat (R-U)", + "_v_to_z": "Miasto/powiat (V-Z)", + "corona_filter": "Usu\u0144 ostrze\u017cenia o koronawirusie", + "slots": "Maksymalna liczba ostrze\u017ce\u0144 na miasto/powiat" + }, + "title": "Opcje" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pt-BR.json b/homeassistant/components/nina/translations/pt-BR.json index c22d3e06530..3da9c79f05f 100644 --- a/homeassistant/components/nina/translations/pt-BR.json +++ b/homeassistant/components/nina/translations/pt-BR.json @@ -23,5 +23,27 @@ "title": "Selecione a cidade/munic\u00edpio" } } + }, + "options": { + "error": { + "cannot_connect": "Falhou ao conectar", + "no_selection": "Selecione pelo menos uma cidade/munic\u00edpio", + "unknown": "Erro inesperado" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Cidade/munic\u00edpio (A-D)", + "_e_to_h": "Cidade/munic\u00edpio (E-H)", + "_i_to_l": "Cidade/munic\u00edpio (I-L)", + "_m_to_q": "Cidade/munic\u00edpio (M-Q)", + "_r_to_u": "Cidade/munic\u00edpio (R-U)", + "_v_to_z": "Cidade/munic\u00edpio (V-Z)", + "corona_filter": "Remover avisos de corona", + "slots": "M\u00e1ximo de avisos por cidade/munic\u00edpio" + }, + "title": "Op\u00e7\u00f5es" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pt.json b/homeassistant/components/nina/translations/pt.json new file mode 100644 index 00000000000..f86b761e487 --- /dev/null +++ b/homeassistant/components/nina/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "title": "Selecione a cidade/distrito" + } + } + }, + "options": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ru.json b/homeassistant/components/nina/translations/ru.json index 114f9d44040..461637eabfc 100644 --- a/homeassistant/components/nina/translations/ru.json +++ b/homeassistant/components/nina/translations/ru.json @@ -23,5 +23,27 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433" } } + }, + "options": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "no_selection": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0445\u043e\u0442\u044f \u0431\u044b \u043e\u0434\u0438\u043d \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (A-D)", + "_e_to_h": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (E-H)", + "_i_to_l": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (I-L)", + "_m_to_q": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (M-Q)", + "_r_to_u": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (R-U)", + "_v_to_z": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (V-Z)", + "corona_filter": "\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f \u043e \u043a\u043e\u0440\u043e\u043d\u0430\u0432\u0438\u0440\u0443\u0441\u0435", + "slots": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0439 \u043d\u0430 \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/tr.json b/homeassistant/components/nina/translations/tr.json index 5fffe4c4f69..5311219271b 100644 --- a/homeassistant/components/nina/translations/tr.json +++ b/homeassistant/components/nina/translations/tr.json @@ -23,5 +23,27 @@ "title": "\u015eehir/il\u00e7e se\u00e7in" } } + }, + "options": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_selection": "L\u00fctfen en az bir \u015fehir/il\u00e7e se\u00e7in", + "unknown": "Beklenmeyen hata" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u015eehir/il\u00e7e (A-D)", + "_e_to_h": "\u015eehir/il\u00e7e (E-H)", + "_i_to_l": "\u015eehir/il\u00e7e (I-L)", + "_m_to_q": "\u015eehir/il\u00e7e (M-Q)", + "_r_to_u": "\u015eehir/il\u00e7e (R-U)", + "_v_to_z": "\u015eehir/il\u00e7e (V-Z)", + "corona_filter": "Korona Uyar\u0131lar\u0131n\u0131 Kald\u0131r", + "slots": "\u015eehir/il\u00e7e ba\u015f\u0131na maksimum uyar\u0131 say\u0131s\u0131" + }, + "title": "Se\u00e7enekler" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/zh-Hant.json b/homeassistant/components/nina/translations/zh-Hant.json index 212c65070d8..dde251012a0 100644 --- a/homeassistant/components/nina/translations/zh-Hant.json +++ b/homeassistant/components/nina/translations/zh-Hant.json @@ -23,5 +23,27 @@ "title": "\u9078\u64c7\u57ce\u5e02/\u7e23\u5e02" } } + }, + "options": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_selection": "\u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u500b\u57ce\u5e02/\u7e23\u5e02", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u57ce\u5e02/\u7e23\u5e02\uff08A-D\uff09", + "_e_to_h": "\u57ce\u5e02/\u7e23\u5e02\uff08E-H\uff09", + "_i_to_l": "\u57ce\u5e02/\u7e23\u5e02\uff08I-L\uff09", + "_m_to_q": "\u57ce\u5e02/\u7e23\u5e02\uff08M-Q\uff09", + "_r_to_u": "\u57ce\u5e02/\u7e23\u5e02\uff08R-U\uff09", + "_v_to_z": "\u57ce\u5e02/\u7e23\u5e02\uff08V-Z\uff09", + "corona_filter": "\u79fb\u9664 Corona \u8b66\u544a", + "slots": "\u6bcf\u500b\u57ce\u5e02/\u7e23\u5e02\u6700\u5927\u8b66\u544a\u503c" + }, + "title": "\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 21469f197f4..5f3333ec750 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner = domain_data[entry.entry_id] = NmapDeviceScanner(hass, entry, devices) await scanner.async_setup() entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nmap_tracker/translations/pt.json b/homeassistant/components/nmap_tracker/translations/pt.json new file mode 100644 index 00000000000..8215a0f7a01 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/pt.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "Intervalo de varrimento" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index bc8a7bffa95..788c698c0ca 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -39,6 +39,13 @@ PLATFORM_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" + + # We need to add the component here break the deadlock + # when setting up integrations from config entries as + # they would otherwise wait for notify to be + # setup and thus the config entries would not be able to + # setup their platforms. + hass.config.components.add(DOMAIN) await async_setup_legacy(hass, config) async def persistent_notification(service: ServiceCall) -> None: diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index e8bd2c725d5..2a73d12d946 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -118,13 +118,18 @@ def _async_register_new_bridge( hass: HomeAssistant, bridge: dict, entry: ConfigEntry ) -> None: """Register a new bridge.""" + if name := bridge["name"]: + bridge_name = name.capitalize() + else: + bridge_name = bridge["id"] + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, bridge["hardware_id"])}, manufacturer="Silicon Labs", model=bridge["hardware_revision"], - name=bridge["name"] or bridge["id"], + name=bridge_name, sw_version=bridge["firmware_version"]["wifi"], ) @@ -132,6 +137,8 @@ def _async_register_new_bridge( class NotionEntity(CoordinatorEntity): """Define a base Notion entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, @@ -150,13 +157,12 @@ class NotionEntity(CoordinatorEntity): identifiers={(DOMAIN, sensor["hardware_id"])}, manufacturer="Silicon Labs", model=sensor["hardware_revision"], - name=str(sensor["name"]), + name=str(sensor["name"]).capitalize(), sw_version=sensor["firmware_version"], via_device=(DOMAIN, bridge.get("hardware_id")), ) self._attr_extra_state_attributes = {} - self._attr_name = f'{sensor["name"]}: {description.name}' self._attr_unique_id = ( f'{sensor_id}_{coordinator.data["tasks"][task_id]["task_type"]}' ) diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 57c70849a9a..2a34724837d 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -48,7 +48,7 @@ class NotionBinarySensorDescription( BINARY_SENSOR_DESCRIPTIONS = ( NotionBinarySensorDescription( key=SENSOR_BATTERY, - name="Low Battery", + name="Low battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state="critical", @@ -61,13 +61,13 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), NotionBinarySensorDescription( key=SENSOR_GARAGE_DOOR, - name="Garage Door", + name="Garage door", device_class=BinarySensorDeviceClass.GARAGE_DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_LEAK, - name="Leak Detector", + name="Leak detector", device_class=BinarySensorDeviceClass.MOISTURE, on_state="leak", ), @@ -86,25 +86,25 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), NotionBinarySensorDescription( key=SENSOR_SLIDING, - name="Sliding Door/Window", + name="Sliding door/window", device_class=BinarySensorDeviceClass.DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_SMOKE_CO, - name="Smoke/Carbon Monoxide Detector", + name="Smoke/Carbon monoxide detector", device_class=BinarySensorDeviceClass.SMOKE, on_state="alarm", ), NotionBinarySensorDescription( key=SENSOR_WINDOW_HINGED_HORIZONTAL, - name="Hinged Window", + name="Hinged window", device_class=BinarySensorDeviceClass.WINDOW, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_WINDOW_HINGED_VERTICAL, - name="Hinged Window", + name="Hinged window", device_class=BinarySensorDeviceClass.WINDOW, on_state="open", ), diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json index 8cd2a58e1e0..2171d2722a1 100644 --- a/homeassistant/components/notion/translations/ja.json +++ b/homeassistant/components/notion/translations/ja.json @@ -14,7 +14,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5ea6\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/notion/translations/pt.json b/homeassistant/components/notion/translations/pt.json index e92d51b2058..4ce3e317e32 100644 --- a/homeassistant/components/notion/translations/pt.json +++ b/homeassistant/components/notion/translations/pt.json @@ -7,6 +7,12 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index fe5d27feee2..553f73a128e 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -1,15 +1,19 @@ """Support for NSW Rural Fire Service Feeds.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import logging +from typing import Any from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeedManager +from aio_geojson_nsw_rfs_incidents.feed_entry import ( + NswRuralFireServiceIncidentsFeedEntry, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_LATITUDE, CONF_LONGITUDE, @@ -19,7 +23,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -73,23 +77,23 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the NSW Rural Fire Service Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - categories = config.get(CONF_CATEGORIES) + radius_in_km: float = config[CONF_RADIUS] + categories: list[str] = config[CONF_CATEGORIES] # Initialize the entity manager. manager = NswRuralFireServiceFeedEntityManager( hass, async_add_entities, scan_interval, coordinates, radius_in_km, categories ) - async def start_feed_manager(event): + async def start_feed_manager(event: Event) -> None: """Start feed manager.""" await manager.async_init() - async def stop_feed_manager(event): + async def stop_feed_manager(event: Event) -> None: """Stop feed manager.""" await manager.async_stop() @@ -103,13 +107,13 @@ class NswRuralFireServiceFeedEntityManager: def __init__( self, - hass, - async_add_entities, - scan_interval, - coordinates, - radius_in_km, - categories, - ): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + radius_in_km: float, + categories: list[str], + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass websession = aiohttp_client.async_get_clientsession(hass) @@ -124,12 +128,12 @@ class NswRuralFireServiceFeedEntityManager: ) self._async_add_entities = async_add_entities self._scan_interval = scan_interval - self._track_time_remove_callback = None + self._track_time_remove_callback: Callable[[], None] | None = None - async def async_init(self): + async def async_init(self) -> None: """Schedule initial and regular updates based on configured time interval.""" - async def update(event_time): + async def update(event_time: datetime) -> None: """Update.""" await self.async_update() @@ -140,32 +144,34 @@ class NswRuralFireServiceFeedEntityManager: _LOGGER.debug("Feed entity manager initialized") - async def async_update(self): + async def async_update(self) -> None: """Refresh data.""" await self._feed_manager.update() _LOGGER.debug("Feed entity manager updated") - async def async_stop(self): + async def async_stop(self) -> None: """Stop this feed entity manager from refreshing.""" if self._track_time_remove_callback: self._track_time_remove_callback() _LOGGER.debug("Feed entity manager stopped") - def get_entry(self, external_id): + def get_entry( + self, external_id: str + ) -> NswRuralFireServiceIncidentsFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - async def _generate_entity(self, external_id): + async def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = NswRuralFireServiceLocationEvent(self, external_id) # Add new entities to HA. self._async_add_entities([new_entity], True) - async def _update_entity(self, external_id): + async def _update_entity(self, external_id: str) -> None: """Update entity.""" async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - async def _remove_entity(self, external_id): + async def _remove_entity(self, external_id: str) -> None: """Remove entity.""" async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) @@ -173,15 +179,16 @@ class NswRuralFireServiceFeedEntityManager: class NswRuralFireServiceLocationEvent(GeolocationEvent): """This represents an external event with NSW Rural Fire Service data.""" - def __init__(self, feed_manager, external_id): + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__( + self, feed_manager: NswRuralFireServiceFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._category = None self._publication_date = None self._location = None @@ -191,10 +198,10 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._fire = None self._size = None self._responsible_agency = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -213,34 +220,31 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._remove_signal_update() @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for NSW Rural Fire Service location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed( + self, feed_entry: NswRuralFireServiceIncidentsFeedEntry + ) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._category = feed_entry.category self._publication_date = feed_entry.publication_date self._location = feed_entry.location @@ -252,51 +256,20 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._responsible_agency = feed_entry.responsible_agency @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend.""" if self._fire: return "mdi:fire" return "mdi:alarm-light" @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), (ATTR_CATEGORY, self._category), (ATTR_LOCATION, self._location), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_PUBLICATION_DATE, self._publication_date), (ATTR_COUNCIL_AREA, self._council_area), (ATTR_STATUS, self._status), diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 06796e836b4..86574920cd0 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nuheat/translations/pt.json b/homeassistant/components/nuheat/translations/pt.json index 7953cf5625c..16a664b4225 100644 --- a/homeassistant/components/nuheat/translations/pt.json +++ b/homeassistant/components/nuheat/translations/pt.json @@ -12,6 +12,7 @@ "user": { "data": { "password": "Palavra-passe", + "serial_number": "N\u00famero de s\u00e9rie do termostato.", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index a59b0a62f70..dcb359f32d2 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -147,7 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nuki/translations/ja.json b/homeassistant/components/nuki/translations/ja.json index 6f54d0d8b4b..d4e1d68d780 100644 --- a/homeassistant/components/nuki/translations/ja.json +++ b/homeassistant/components/nuki/translations/ja.json @@ -13,8 +13,8 @@ "data": { "token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, - "description": "Nuki\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001bridge\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Nuki\u7d71\u5408\u3067\u306f\u3001bridge\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/nuki/translations/pt.json b/homeassistant/components/nuki/translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/nuki/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index fe438ea6aea..4990fbfa7f8 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -14,8 +14,13 @@ import voluptuous as vol from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_MODE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.const import ( + ATTR_MODE, + CONF_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -69,6 +74,10 @@ UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { NumberDeviceClass.TEMPERATURE: temperature_util.convert, } +VALID_UNITS: dict[str, tuple[str, ...]] = { + NumberDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, +} + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" @@ -143,7 +152,7 @@ class NumberEntityDescription(EntityDescription): else: module = inspect.getmodule(self) if module and module.__file__ and "custom_components" in module.__file__: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " @@ -183,16 +192,18 @@ class NumberEntity(Entity): entity_description: NumberEntityDescription _attr_max_value: None _attr_min_value: None + _attr_mode: NumberMode = NumberMode.AUTO _attr_state: None = None _attr_step: None - _attr_mode: NumberMode = NumberMode.AUTO + _attr_unit_of_measurement: None # Subclasses of NumberEntity should not set this _attr_value: None _attr_native_max_value: float _attr_native_min_value: float _attr_native_step: float - _attr_native_value: float + _attr_native_value: float | None = None _attr_native_unit_of_measurement: str | None _deprecated_number_entity_reported = False + _number_option_unit_of_measurement: str | None = None def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" @@ -211,7 +222,7 @@ class NumberEntity(Entity): ): module = inspect.getmodule(cls) if module and module.__file__ and "custom_components" in module.__file__: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " @@ -226,6 +237,13 @@ class NumberEntity(Entity): report_issue, ) + async def async_internal_added_to_hass(self) -> None: + """Call when the number entity is added to hass.""" + await super().async_internal_added_to_hass() + if not self.registry_entry: + return + self.async_registry_entry_updated() + @property def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" @@ -348,7 +366,11 @@ class NumberEntity(Entity): @final def unit_of_measurement(self) -> str | None: """Return the unit of measurement of the entity, after unit conversion.""" + if self._number_option_unit_of_measurement: + return self._number_option_unit_of_measurement + if hasattr(self, "_attr_unit_of_measurement"): + self._report_deprecated_number_entity() return self._attr_unit_of_measurement if ( hasattr(self, "entity_description") @@ -467,6 +489,22 @@ class NumberEntity(Entity): report_issue, ) + @callback + def async_registry_entry_updated(self) -> None: + """Run when the entity registry entry has been updated.""" + assert self.registry_entry + if ( + (number_options := self.registry_entry.options.get(DOMAIN)) + and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT)) + and (device_class := self.device_class) in UNIT_CONVERSIONS + and self.native_unit_of_measurement in VALID_UNITS[device_class] + and custom_unit in VALID_UNITS[device_class] + ): + self._number_option_unit_of_measurement = custom_unit + return + + self._number_option_unit_of_measurement = None + @dataclasses.dataclass class NumberExtraStoredData(ExtraStoredData): diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 28c41ccda3a..27332e50b18 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=data.device_info.firmware, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 3667acf975e..fe0d0b97c69 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -159,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator_forecast.async_refresh() await coordinator_forecast_hourly.async_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nws/translations/pt.json b/homeassistant/components/nws/translations/pt.json index 2447be7ee67..3d9fdf5a2d3 100644 --- a/homeassistant/components/nws/translations/pt.json +++ b/homeassistant/components/nws/translations/pt.json @@ -4,7 +4,7 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index a29ea829bbc..c5512076172 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) _async_register_services(hass, coordinator) diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index 5851bb21b41..c037619d31b 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -1,6 +1,8 @@ """Provides the NZBGet DataUpdateCoordinator.""" +from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any from async_timeout import timeout from pynzbgetapi import NZBGetAPI, NZBGetAPIException @@ -25,7 +27,13 @@ _LOGGER = logging.getLogger(__name__) class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching NZBGet data.""" - def __init__(self, hass: HomeAssistant, *, config: dict, options: dict) -> None: + def __init__( + self, + hass: HomeAssistant, + *, + config: Mapping[str, Any], + options: Mapping[str, Any], + ) -> None: """Initialize global NZBGet data updater.""" self.nzbget = NZBGetAPI( config[CONF_HOST], @@ -37,7 +45,7 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): ) self._completed_downloads_init = False - self._completed_downloads = {} + self._completed_downloads = set[tuple]() update_interval = timedelta(seconds=options[CONF_SCAN_INTERVAL]) diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json index c6b485976a7..43a741977f7 100644 --- a/homeassistant/components/nzbget/translations/ja.json +++ b/homeassistant/components/nzbget/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/nzbget/translations/pt.json b/homeassistant/components/nzbget/translations/pt.json index ba038c72c68..4bbc3a839fd 100644 --- a/homeassistant/components/nzbget/translations/pt.json +++ b/homeassistant/components/nzbget/translations/pt.json @@ -15,7 +15,7 @@ "name": "Nome", "password": "Palavra-passe", "port": "Porta", - "ssl": "NZBGet usa um certificado SSL", + "ssl": "Utiliza um certificado SSL", "username": "Nome de Utilizador", "verify_ssl": "NZBGet usa um certificado adequado" }, diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index eded3bec16a..1d1c1958420 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -182,7 +182,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "client": client, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/octoprint/translations/pt.json b/homeassistant/components/octoprint/translations/pt.json new file mode 100644 index 00000000000..7fef03088cb --- /dev/null +++ b/homeassistant/components/octoprint/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "unknown": "Erro inesperado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index 8a55eff6bb0..27f145f82b6 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: OMNI_API: api, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 4c92420972b..e28fd53d6fe 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from typing import Any from omnilogic import OmniLogic, OmniLogicException @@ -122,7 +123,7 @@ class OmniLogicEntity(CoordinatorEntity[OmniLogicUpdateCoordinator]): self._unique_id = unique_id self._item_id = item_id self._icon = icon - self._attrs = {} + self._attrs: dict[str, Any] = {} self._msp_system_id = msp_system_id self._backyard_name = coordinator.data[backyard_id]["BackyardName"] diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index ce9b29ef1d4..04bb1abf3e8 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -1,4 +1,6 @@ """Definition and setup of the Omnilogic Sensors for Home Assistant.""" +from typing import Any + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -34,7 +36,8 @@ async def async_setup_entry( continue for entity_setting in entity_settings: - for state_key, entity_class in entity_setting["entity_classes"].items(): + entity_classes: dict[str, type] = entity_setting["entity_classes"] + for state_key, entity_class in entity_classes.items(): if check_guard(state_key, item, entity_setting): continue @@ -248,7 +251,7 @@ class OmniLogicORPSensor(OmnilogicSensor): return orp_state -SENSOR_TYPES = { +SENSOR_TYPES: dict[tuple[int, str], list[dict[str, Any]]] = { (2, "Backyard"): [ { "entity_classes": {"airTemp": OmniLogicTemperatureSensor}, diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index 3edb4a20d33..2d2ad08d38a 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -1,5 +1,6 @@ """Platform for Omnilogic switch integration.""" import time +from typing import Any from omnilogic import OmniLogicException import voluptuous as vol @@ -34,7 +35,8 @@ async def async_setup_entry( continue for entity_setting in entity_settings: - for state_key, entity_class in entity_setting["entity_classes"].items(): + entity_classes: dict[str, type] = entity_setting["entity_classes"] + for state_key, entity_class in entity_classes.items(): if check_guard(state_key, item, entity_setting): continue @@ -229,7 +231,7 @@ class OmniLogicPumpControl(OmniLogicSwitch): raise OmniLogicException("Cannot set speed on a non-variable speed pump.") -SWITCH_TYPES = { +SWITCH_TYPES: dict[tuple[int, str], list[dict[str, Any]]] = { (4, "Relays"): [ { "entity_classes": {"switchState": OmniLogicRelayControl}, diff --git a/homeassistant/components/omnilogic/translations/ja.json b/homeassistant/components/omnilogic/translations/ja.json index c8f97ac3a40..f66f0a4878f 100644 --- a/homeassistant/components/omnilogic/translations/ja.json +++ b/homeassistant/components/omnilogic/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/oncue/__init__.py b/homeassistant/components/oncue/__init__.py index 24a2ec24c7d..eb9ac37db18 100644 --- a/homeassistant/components/oncue/__init__.py +++ b/homeassistant/components/oncue/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/oncue/translations/pt.json b/homeassistant/components/oncue/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/oncue/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index e827b32f48a..5dccca54772 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ondilo_ico/translations/pt.json b/homeassistant/components/ondilo_ico/translations/pt.json new file mode 100644 index 00000000000..b7b721c012c --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index b836d7e3298..e3454a5eb5c 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = onewire_hub - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(options_update_listener)) diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 75b01a0cbc9..04e6bbac0cf 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -12,7 +12,7 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" }, - "title": "1-Wire\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "\u30b5\u30fc\u30d0\u30fc\u306e\u8a73\u7d30\u3092\u8a2d\u5b9a\u3059\u308b" } } }, diff --git a/homeassistant/components/onewire/translations/pt.json b/homeassistant/components/onewire/translations/pt.json index db0e0c2a137..fa5aa3de317 100644 --- a/homeassistant/components/onewire/translations/pt.json +++ b/homeassistant/components/onewire/translations/pt.json @@ -5,6 +5,14 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 7922d59ca53..ac20a564c8a 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if device.capabilities.events: platforms += [Platform.BINARY_SENSOR, Platform.SENSOR] - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) diff --git a/homeassistant/components/onvif/base.py b/homeassistant/components/onvif/base.py index 93fd6a26b35..f40b6173a3b 100644 --- a/homeassistant/components/onvif/base.py +++ b/homeassistant/components/onvif/base.py @@ -1,42 +1,50 @@ """Base classes for ONVIF entities.""" +from __future__ import annotations + from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .device import ONVIFDevice -from .models import Profile class ONVIFBaseEntity(Entity): """Base class common to all ONVIF entities.""" - def __init__(self, device: ONVIFDevice, profile: Profile = None) -> None: + def __init__(self, device: ONVIFDevice) -> None: """Initialize the ONVIF entity.""" self.device: ONVIFDevice = device - self.profile: Profile = profile @property def available(self): """Return True if device is available.""" return self.device.available + @property + def mac_or_serial(self) -> str: + """Return MAC or serial, for unique_id generation. + + MAC address is not always available, and given the number + of non-conformant ONVIF devices we have historically supported, + we can not guarantee serial number either. Due to this, we have + adopted an either/or approach in the config entry setup, and can + guarantee that one or the other will be populated. + See: https://github.com/home-assistant/core/issues/35883 + """ + return ( + self.device.info.mac + or self.device.info.serial_number # type:ignore[return-value] + ) + @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - connections = None + connections: set[tuple[str, str]] = set() if self.device.info.mac: connections = {(CONNECTION_NETWORK_MAC, self.device.info.mac)} return DeviceInfo( connections=connections, - identifiers={ - # MAC address is not always available, and given the number - # of non-conformant ONVIF devices we have historically supported, - # we can not guarantee serial number either. Due to this, we have - # adopted an either/or approach in the config entry setup, and can - # guarantee that one or the other will be populated. - # See: https://github.com/home-assistant/core/issues/35883 - (DOMAIN, self.device.info.mac or self.device.info.serial_number) - }, + identifiers={(DOMAIN, self.mac_or_serial)}, manufacturer=self.device.info.manufacturer, model=self.device.info.model, name=self.device.name, diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py index 2e72d331d3d..cd9af1d83b5 100644 --- a/homeassistant/components/onvif/binary_sensor.py +++ b/homeassistant/components/onvif/binary_sensor.py @@ -48,15 +48,16 @@ async def async_setup_entry( device.events.async_add_listener(async_check_entities) - return True - class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity): """Representation of a binary ONVIF event.""" _attr_should_poll = False + _attr_unique_id: str - def __init__(self, uid, device: ONVIFDevice, entry: er.RegistryEntry | None = None): + def __init__( + self, uid: str, device: ONVIFDevice, entry: er.RegistryEntry | None = None + ) -> None: """Initialize the ONVIF binary sensor.""" self._attr_unique_id = uid if entry is not None: @@ -65,6 +66,7 @@ class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity): self._attr_name = entry.name else: event = device.events.get_uid(uid) + assert event self._attr_device_class = event.device_class self._attr_entity_category = event.entity_category self._attr_entity_registry_enabled_default = event.entity_enabled diff --git a/homeassistant/components/onvif/button.py b/homeassistant/components/onvif/button.py index 23ea5124e61..2732c672ad9 100644 --- a/homeassistant/components/onvif/button.py +++ b/homeassistant/components/onvif/button.py @@ -1,5 +1,4 @@ """ONVIF Buttons.""" - from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,7 +17,7 @@ async def async_setup_entry( ) -> None: """Set up ONVIF button based on a config entry.""" device = hass.data[DOMAIN][config_entry.unique_id] - async_add_entities([RebootButton(device)]) + async_add_entities([RebootButton(device), SetSystemDateAndTimeButton(device)]) class RebootButton(ONVIFBaseEntity, ButtonEntity): @@ -31,11 +30,25 @@ class RebootButton(ONVIFBaseEntity, ButtonEntity): """Initialize the button entity.""" super().__init__(device) self._attr_name = f"{self.device.name} Reboot" - self._attr_unique_id = ( - f"{self.device.info.mac or self.device.info.serial_number}_reboot" - ) + self._attr_unique_id = f"{self.mac_or_serial}_reboot" async def async_press(self) -> None: """Send out a SystemReboot command.""" device_mgmt = self.device.device.create_devicemgmt_service() await device_mgmt.SystemReboot() + + +class SetSystemDateAndTimeButton(ONVIFBaseEntity, ButtonEntity): + """Defines a ONVIF SetSystemDateAndTime button.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, device: ONVIFDevice) -> None: + """Initialize the button entity.""" + super().__init__(device) + self._attr_name = f"{self.device.name} Set System Date and Time" + self._attr_unique_id = f"{self.mac_or_serial}_setsystemdatetime" + + async def async_press(self) -> None: + """Send out a SetSystemDateAndTime command.""" + await self.device.async_manually_set_date_and_time() diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 6aa7ae42767..5aa49f68aa6 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -13,6 +13,7 @@ from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, ) +from homeassistant.components.stream.const import RTSP_TRANSPORTS from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.core import HomeAssistant @@ -46,6 +47,8 @@ from .const import ( ZOOM_IN, ZOOM_OUT, ) +from .device import ONVIFDevice +from .models import Profile async def async_setup_entry( @@ -85,20 +88,19 @@ async def async_setup_entry( [ONVIFCameraEntity(device, profile) for profile in device.profiles] ) - return True - class ONVIFCameraEntity(ONVIFBaseEntity, Camera): """Representation of an ONVIF camera.""" _attr_supported_features = CameraEntityFeature.STREAM - def __init__(self, device, profile): + def __init__(self, device: ONVIFDevice, profile: Profile) -> None: """Initialize ONVIF camera entity.""" - ONVIFBaseEntity.__init__(self, device, profile) + ONVIFBaseEntity.__init__(self, device) Camera.__init__(self) + self.profile = profile self.stream_options[CONF_RTSP_TRANSPORT] = device.config_entry.options.get( - CONF_RTSP_TRANSPORT + CONF_RTSP_TRANSPORT, next(iter(RTSP_TRANSPORTS)) ) self.stream_options[ CONF_USE_WALLCLOCK_AS_TIMESTAMPS @@ -118,8 +120,8 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): def unique_id(self) -> str: """Return a unique ID.""" if self.profile.index: - return f"{self.device.info.mac or self.device.info.serial_number}_{self.profile.index}" - return self.device.info.mac or self.device.info.serial_number + return f"{self.mac_or_serial}_{self.profile.index}" + return self.mac_or_serial @property def entity_registry_enabled_default(self) -> bool: @@ -149,6 +151,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): ) if image is None: + assert self._stream_uri return await ffmpeg.async_get_image( self.hass, self._stream_uri, diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index d376b7fe258..dda28e07a2a 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -5,6 +5,7 @@ import asyncio from contextlib import suppress import datetime as dt import os +import time from httpx import RequestError import onvif @@ -41,21 +42,21 @@ from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video class ONVIFDevice: """Manages an ONVIF device.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None) -> None: + device: ONVIFCamera + events: EventManager + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the device.""" self.hass: HomeAssistant = hass self.config_entry: ConfigEntry = config_entry self.available: bool = True - self.device: ONVIFCamera = None - self.events: EventManager = None - self.info: DeviceInfo = DeviceInfo() self.capabilities: Capabilities = Capabilities() self.profiles: list[Profile] = [] self.max_resolution: int = 0 - self._dt_diff_seconds: int = 0 + self._dt_diff_seconds: float = 0 @property def name(self) -> str: @@ -98,6 +99,7 @@ class ONVIFDevice: await self.async_check_date_and_time() # Create event manager + assert self.config_entry.unique_id self.events = EventManager( self.hass, self.device, self.config_entry.unique_id ) @@ -148,6 +150,32 @@ class ONVIFDevice: await self.events.async_stop() await self.device.close() + async def async_manually_set_date_and_time(self) -> None: + """Set Date and Time Manually using SetSystemDateAndTime command.""" + device_mgmt = self.device.create_devicemgmt_service() + + # Retrieve DateTime object from camera to use as template for Set operation + device_time = await device_mgmt.GetSystemDateAndTime() + + system_date = dt_util.utcnow() + LOGGER.debug("System date (UTC): %s", system_date) + + dt_param = device_mgmt.create_type("SetSystemDateAndTime") + dt_param.DateTimeType = "Manual" + # Retrieve DST setting from system + dt_param.DaylightSavings = bool(time.localtime().tm_isdst) + dt_param.UTCDateTime = device_time.UTCDateTime + # Retrieve timezone from system + dt_param.TimeZone = str(system_date.astimezone().tzinfo) + dt_param.UTCDateTime.Date.Year = system_date.year + dt_param.UTCDateTime.Date.Month = system_date.month + dt_param.UTCDateTime.Date.Day = system_date.day + dt_param.UTCDateTime.Time.Hour = system_date.hour + dt_param.UTCDateTime.Time.Minute = system_date.minute + dt_param.UTCDateTime.Time.Second = system_date.second + LOGGER.debug("SetSystemDateAndTime: %s", dt_param) + await device_mgmt.SetSystemDateAndTime(dt_param) + async def async_check_date_and_time(self) -> None: """Warns if device and system date not synced.""" LOGGER.debug("Setting up the ONVIF device management service") @@ -165,6 +193,8 @@ class ONVIFDevice: ) return + LOGGER.debug("Device time: %s", device_time) + tzone = dt_util.DEFAULT_TIME_ZONE cdate = device_time.LocalDateTime if device_time.UTCDateTime: @@ -207,6 +237,9 @@ class ONVIFDevice: cam_date_utc, system_date, ) + if device_time.DateTimeType == "Manual": + # Set Date and Time ourselves if Date and Time is set manually in the camera. + await self.async_manually_set_date_and_time() except RequestError as err: LOGGER.warning( "Couldn't get device '%s' date/time. Error: %s", self.name, err @@ -265,7 +298,7 @@ class ONVIFDevice: """Obtain media profiles for this device.""" media_service = self.device.create_media_service() result = await media_service.GetProfiles() - profiles = [] + profiles: list[Profile] = [] if not isinstance(result, list): return profiles @@ -364,7 +397,7 @@ class ONVIFDevice: req.ProfileToken = profile.token if move_mode == CONTINUOUS_MOVE: # Guard against unsupported operation - if not profile.ptz.continuous: + if not profile.ptz or not profile.ptz.continuous: LOGGER.warning( "ContinuousMove not supported on device '%s'", self.name ) @@ -387,7 +420,7 @@ class ONVIFDevice: ) elif move_mode == RELATIVE_MOVE: # Guard against unsupported operation - if not profile.ptz.relative: + if not profile.ptz or not profile.ptz.relative: LOGGER.warning( "RelativeMove not supported on device '%s'", self.name ) @@ -404,7 +437,7 @@ class ONVIFDevice: await ptz_service.RelativeMove(req) elif move_mode == ABSOLUTE_MOVE: # Guard against unsupported operation - if not profile.ptz.absolute: + if not profile.ptz or not profile.ptz.absolute: LOGGER.warning( "AbsoluteMove not supported on device '%s'", self.name ) @@ -421,6 +454,11 @@ class ONVIFDevice: await ptz_service.AbsoluteMove(req) elif move_mode == GOTOPRESET_MOVE: # Guard against unsupported operation + if not profile.ptz or not profile.ptz.presets: + LOGGER.warning( + "Absolute Presets not supported on device '%s'", self.name + ) + return if preset_val not in profile.ptz.presets: LOGGER.warning( "PTZ preset '%s' does not exist on device '%s'. Available Presets: %s", diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index 67c0ee3da3f..b662dca1d5d 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import date, datetime +from decimal import Decimal from homeassistant.components.sensor import RestoreSensor from homeassistant.config_entries import ConfigEntry @@ -47,8 +48,6 @@ async def async_setup_entry( device.events.async_add_listener(async_check_entities) - return True - class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): """Representation of a ONVIF sensor event.""" @@ -65,6 +64,7 @@ class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): self._attr_native_unit_of_measurement = entry.unit_of_measurement else: event = device.events.get_uid(uid) + assert event self._attr_device_class = event.device_class self._attr_entity_category = event.entity_category self._attr_entity_registry_enabled_default = event.entity_enabled @@ -75,7 +75,7 @@ class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): super().__init__(device) @property - def native_value(self) -> StateType | date | datetime: + def native_value(self) -> StateType | date | datetime | Decimal: """Return the value reported by the sensor.""" if (event := self.device.events.get_uid(self._attr_unique_id)) is not None: return event.value diff --git a/homeassistant/components/onvif/translations/pt.json b/homeassistant/components/onvif/translations/pt.json index f79bbec3201..4240578ca47 100644 --- a/homeassistant/components/onvif/translations/pt.json +++ b/homeassistant/components/onvif/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "O dispositivo ONVIF j\u00e1 est\u00e1 configurado.", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo ONVIF j\u00e1 est\u00e1 em andamento.", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "no_h264": "N\u00e3o existem fluxos H264 dispon\u00edveis. Verifique a configura\u00e7\u00e3o de perfil no seu dispositivo.", "no_mac": "N\u00e3o foi poss\u00edvel configurar o ID unico para o dispositivo ONVIF.", "onvif_error": "Erro ao configurar o dispositivo ONVIF. Verifique os logs para obter mais informa\u00e7\u00f5es." @@ -11,6 +11,11 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "configure": { + "data": { + "host": "Servidor" + } + }, "configure_profile": { "data": { "include": "Criar entidade da c\u00e2mara" diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py index de42d19d8c9..4dc6a45e16c 100644 --- a/homeassistant/components/open_meteo/__init__.py +++ b/homeassistant/components/open_meteo/__init__.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index d1f0adf2e87..6af9900ec15 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -37,6 +37,7 @@ class OpenMeteoWeatherEntity( ): """Defines an Open-Meteo weather entity.""" + _attr_has_entity_name = True _attr_native_precipitation_unit = LENGTH_MILLIMETERS _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR @@ -50,7 +51,6 @@ class OpenMeteoWeatherEntity( """Initialize Open-Meteo weather entity.""" super().__init__(coordinator=coordinator) self._attr_unique_id = entry.entry_id - self._attr_name = entry.title self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index e24af7c7d1f..06688b3b297 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import io +import logging import re import voluptuous as vol @@ -13,6 +14,7 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, ) +from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -26,6 +28,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.async_ import run_callback_threadsafe +_LOGGER = logging.getLogger(__name__) + RE_ALPR_PLATE = re.compile(r"^plate\d*:") RE_ALPR_RESULT = re.compile(r"- (\w*)\s*confidence: (\d*.\d*)") @@ -69,6 +73,18 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the OpenALPR local platform.""" + create_issue( + hass, + "openalpr_local", + "pending_removal", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The OpenALPR Local is deprecated and will be removed in Home Assistant 2022.10" + ) command = [config[CONF_ALPR_BIN], "-c", config[CONF_REGION], "-"] confidence = config[CONF_CONFIDENCE] diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 8837d79369d..5243aa2b282 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -3,5 +3,6 @@ "name": "OpenALPR Local", "documentation": "https://www.home-assistant.io/integrations/openalpr_local", "codeowners": [], + "dependencies": ["repairs"], "iot_class": "local_push" } diff --git a/homeassistant/components/openalpr_local/strings.json b/homeassistant/components/openalpr_local/strings.json new file mode 100644 index 00000000000..b0dc80c6d06 --- /dev/null +++ b/homeassistant/components/openalpr_local/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The OpenALPR Local integration is being removed", + "description": "The OpenALPR Local integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/openalpr_local/translations/de.json b/homeassistant/components/openalpr_local/translations/de.json new file mode 100644 index 00000000000..d517fe0b37f --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die lokale OpenALPR-Integration wird derzeit aus dem Home Assistant entfernt und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die lokale OpenALPR-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/en.json b/homeassistant/components/openalpr_local/translations/en.json new file mode 100644 index 00000000000..9bc9035515b --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The OpenALPR Local integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The OpenALPR Local integration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/it.json b/homeassistant/components/openalpr_local/translations/it.json new file mode 100644 index 00000000000..26ce80ee584 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione OpenALPR Local \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "L'integrazione OpenALPR Local verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pl.json b/homeassistant/components/openalpr_local/translations/pl.json new file mode 100644 index 00000000000..ac367d20809 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja OpenALPR Local oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja OpenALPR Local zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pt-BR.json b/homeassistant/components/openalpr_local/translations/pt-BR.json new file mode 100644 index 00000000000..96b2c244b5c --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/zh-Hant.json b/homeassistant/components/openalpr_local/translations/zh-Hant.json new file mode 100644 index 00000000000..8ec55e5a004 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 0272feb0f9e..cada1199084 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.23.0", "opencv-python-headless==4.6.0.66"], + "requirements": ["numpy==1.23.1", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index eb1b50db5b6..6b97e88df0b 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await open_garage_data_coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = open_garage_data_coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/opengarage/translations/pt.json b/homeassistant/components/opengarage/translations/pt.json new file mode 100644 index 00000000000..1ebfd763b2c --- /dev/null +++ b/homeassistant/components/opengarage/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "port": "Porta", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 0d01550df22..cdf360c8795 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,9 +1,11 @@ """Support for OpenTherm Gateway devices.""" +import asyncio from datetime import date, datetime import logging import pyotgw import pyotgw.vars as gw_vars +from serial import SerialException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -23,6 +25,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -37,6 +40,7 @@ from .const import ( CONF_PRECISION, CONF_READ_PRECISION, CONF_SET_PRECISION, + CONNECTION_TIMEOUT, DATA_GATEWAYS, DATA_OPENTHERM_GW, DOMAIN, @@ -107,10 +111,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.add_update_listener(options_updated) - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe()) + try: + await asyncio.wait_for( + gateway.connect_and_subscribe(), + timeout=CONNECTION_TIMEOUT, + ) + except (asyncio.TimeoutError, ConnectionError, SerialException) as ex: + raise ConfigEntryNotReady( + f"Could not connect to gateway at {gateway.device_path}: {ex}" + ) from ex - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) register_services(hass) return True @@ -428,6 +439,9 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" self.status = await self.gateway.connect(self.device_path) + if not self.status: + await self.cleanup() + raise ConnectionError version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) self.gw_version = version_string[18:] if version_string else None _LOGGER.debug( diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 3f91496adab..c3a955b2387 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -26,6 +26,7 @@ from .const import ( CONF_READ_PRECISION, CONF_SET_PRECISION, CONF_TEMPORARY_OVRD_MODE, + CONNECTION_TIMEOUT, ) @@ -62,15 +63,21 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): otgw = pyotgw.OpenThermGateway() status = await otgw.connect(device) await otgw.disconnect() + if not status: + raise ConnectionError return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) try: - res = await asyncio.wait_for(test_connection(), timeout=10) - except (asyncio.TimeoutError, SerialException): + await asyncio.wait_for( + test_connection(), + timeout=CONNECTION_TIMEOUT, + ) + except asyncio.TimeoutError: + return self._show_form({"base": "timeout_connect"}) + except (ConnectionError, SerialException): return self._show_form({"base": "cannot_connect"}) - if res: - return self._create_entry(gw_id, name, device) + return self._create_entry(gw_id, name, device) return self._show_form() diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index a5042628529..d72469759f1 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -25,6 +25,8 @@ CONF_READ_PRECISION = "read_precision" CONF_SET_PRECISION = "set_precision" CONF_TEMPORARY_OVRD_MODE = "temporary_override_mode" +CONNECTION_TIMEOUT = 10 + DATA_GATEWAYS = "gateways" DATA_OPENTHERM_GW = "opentherm_gw" diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 0bc69387d0b..02b1604ea11 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.1"], + "requirements": ["pyotgw==2.0.2"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index f53ffeda6f6..a80a059481d 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -12,7 +12,8 @@ "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "id_exists": "Gateway id already exists", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" } }, "options": { diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 774cd05fd9f..29b13ff5258 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = openuv - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @_verify_domain_control async def update_data(_: ServiceCall) -> None: @@ -183,6 +183,8 @@ class OpenUV: class OpenUvEntity(Entity): """Define a generic OpenUV entity.""" + _attr_has_entity_name = True + def __init__(self, openuv: OpenUV, description: EntityDescription) -> None: """Initialize.""" self._attr_extra_state_attributes = {} diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 503d82d32f2..757f0479e01 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -18,7 +18,7 @@ ATTR_PROTECTION_WINDOW_STARTING_UV = "start_uv" BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW = BinarySensorEntityDescription( key=TYPE_PROTECTION_WINDOW, - name="Protection Window", + name="Protection window", icon="mdi:sunglasses", ) diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index f654ed63a6d..3a5bd3c2a47 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -49,68 +49,68 @@ UV_LEVEL_LOW = "Low" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_CURRENT_OZONE_LEVEL, - name="Current Ozone Level", + name="Current ozone level", device_class=SensorDeviceClass.OZONE, native_unit_of_measurement="du", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_INDEX, - name="Current UV Index", + name="Current UV index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_LEVEL, - name="Current UV Level", + name="Current UV level", icon="mdi:weather-sunny", ), SensorEntityDescription( key=TYPE_MAX_UV_INDEX, - name="Max UV Index", + name="Max UV index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_1, - name="Skin Type 1 Safe Exposure Time", + name="Skin type 1 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_2, - name="Skin Type 2 Safe Exposure Time", + name="Skin type 2 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_3, - name="Skin Type 3 Safe Exposure Time", + name="Skin type 3 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_4, - name="Skin Type 4 Safe Exposure Time", + name="Skin type 4 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_5, - name="Skin Type 5 Safe Exposure Time", + name="Skin type 5 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_6, - name="Skin Type 6 Safe Exposure Time", + name="Skin type 6 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/openuv/translations/pt.json b/homeassistant/components/openuv/translations/pt.json index 6433111fe81..64d5de8785a 100644 --- a/homeassistant/components/openuv/translations/pt.json +++ b/homeassistant/components/openuv/translations/pt.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "Chave de API do OpenUV", + "api_key": "Chave da API", "elevation": "Eleva\u00e7\u00e3o", "latitude": "Latitude", "longitude": "Longitude" diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 8fd7aaae7ad..d462e34cd84 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) update_listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index 511d59b2138..b89b1ef0985 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -15,9 +15,9 @@ "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", "mode": "\u30e2\u30fc\u30c9", - "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" + "name": "\u540d\u524d" }, - "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "OpenWeatherMap\u306e\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 1132e269d04..9acdbfb9ec9 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class): platforms[platform].append(device) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) device_registry = dr.async_get(hass) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index a7595065224..6e6e57f12e5 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -1,6 +1,6 @@ { "domain": "overkiz", - "name": "Overkiz (by Somfy)", + "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", "requirements": ["pyoverkiz==1.4.2"], diff --git a/homeassistant/components/overkiz/translations/pt.json b/homeassistant/components/overkiz/translations/pt.json new file mode 100644 index 00000000000..1e3d9138c84 --- /dev/null +++ b/homeassistant/components/overkiz/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.ar.json b/homeassistant/components/overkiz/translations/sensor.ar.json new file mode 100644 index 00000000000..a68781b47cd --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.ar.json @@ -0,0 +1,7 @@ +{ + "state": { + "overkiz__three_way_handle_direction": { + "tilt": "\u0625\u0645\u0627\u0644\u0629" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.id.json b/homeassistant/components/overkiz/translations/sensor.id.json index bf4703507f8..0d0cc6ab7ed 100644 --- a/homeassistant/components/overkiz/translations/sensor.id.json +++ b/homeassistant/components/overkiz/translations/sensor.id.json @@ -39,7 +39,8 @@ }, "overkiz__three_way_handle_direction": { "closed": "Tutup", - "open": "Buka" + "open": "Buka", + "tilt": "Miring" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.pt.json b/homeassistant/components/overkiz/translations/sensor.pt.json new file mode 100644 index 00000000000..fd6f8f53478 --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "overkiz__discrete_rssi_level": { + "normal": "Normal" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.ru.json b/homeassistant/components/overkiz/translations/sensor.ru.json index afd43899b93..94adceab671 100644 --- a/homeassistant/components/overkiz/translations/sensor.ru.json +++ b/homeassistant/components/overkiz/translations/sensor.ru.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "\u0427\u0438\u0441\u0442\u043e", "dirty": "\u0413\u0440\u044f\u0437\u043d\u043e" + }, + "overkiz__three_way_handle_direction": { + "closed": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", + "tilt": "\u041d\u0430\u043a\u043b\u043e\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.tr.json b/homeassistant/components/overkiz/translations/sensor.tr.json index b04b5a793bc..b9e88b93d41 100644 --- a/homeassistant/components/overkiz/translations/sensor.tr.json +++ b/homeassistant/components/overkiz/translations/sensor.tr.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Temiz", "dirty": "Kirli" + }, + "overkiz__three_way_handle_direction": { + "closed": "Kapal\u0131", + "open": "A\u00e7\u0131k", + "tilt": "E\u011fim" } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 9d2623af0f9..cb2ded6fcef 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -77,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() # Setup components - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index 2c794b5cd9d..a58669fe11c 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -11,7 +11,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "Nem siker\u00fclt az OVO Energy hiteles\u00edt\u00e9se. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", + "description": "Nem siker\u00fclt az OVO Energy hiteles\u00edt\u00e9se. K\u00e9rem, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/ovo_energy/translations/pt.json b/homeassistant/components/ovo_energy/translations/pt.json index 7015a44b5f9..15241a5fb65 100644 --- a/homeassistant/components/ovo_energy/translations/pt.json +++ b/homeassistant/components/ovo_energy/translations/pt.json @@ -5,11 +5,13 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, + "flow_title": "{username}", "step": { "reauth": { "data": { "password": "Palavra-passe" - } + }, + "title": "Reautentica\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 5a21862c767..6086ee1efd8 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: webhook.async_register(hass, DOMAIN, "OwnTracks", webhook_id, handle_webhook) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.data[DOMAIN]["unsub"] = async_dispatcher_connect( hass, DOMAIN, async_handle_message diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index 46ca14c81ac..d02571a6a50 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "\n\nUnter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen -> Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links -> Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." + "default": "\n\nUnter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen \u2192 Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links \u2192 Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json index 998478a9cc8..ca30df2b5f0 100644 --- a/homeassistant/components/owntracks/translations/ja.json +++ b/homeassistant/components/owntracks/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\n\nAndroid\u306e\u5834\u5408\u3001[OwnTracks app]({android_url})\u3092\u958b\u304d\u3001\u74b0\u5883\u8a2d\u5b9a -> \u63a5\u7d9a \u306b\u79fb\u52d5\u3057\u3066\u3001\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification(\u8b58\u5225\u60c5\u5831):\n - Username: `''`\n - Device ID: `''`\n\nOS\u306e\u5834\u5408\u3001[OwnTracks app]({ios_url})\u3092\u958b\u304d\u3001\u5de6\u4e0a\u306e(i)\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3057\u3066 -> \u8a2d\u5b9a\u3002\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication(\u8a8d\u8a3c\u3092\u30aa\u30f3\u306b\u3059\u308b)\n - UserID: `''`\n\n{secret}\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index 44a3c855c8c..03055013345 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/p1_monitor/translations/pt.json b/homeassistant/components/p1_monitor/translations/pt.json new file mode 100644 index 00000000000..38336a1d5de --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index e03dca74fb0..79504653a39 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -109,7 +109,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b data={**config, ATTR_DEVICE_INFO: device_info}, ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/peco/__init__.py b/homeassistant/components/peco/__init__.py index 86f69213a1c..ad74200dace 100644 --- a/homeassistant/components/peco/__init__.py +++ b/homeassistant/components/peco/__init__.py @@ -60,7 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/peco/translations/pt.json b/homeassistant/components/peco/translations/pt.json new file mode 100644 index 00000000000..9be1378d3e6 --- /dev/null +++ b/homeassistant/components/peco/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "county": "Distrito" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 29e92a6ffe3..9e574e69f90 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_entry)) @@ -108,6 +108,8 @@ class PluggableAction: class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): """Coordinator to update data.""" + config_entry: ConfigEntry + def __init__(self, hass, api: PhilipsTV, options: Mapping) -> None: """Set up the coordinator.""" self.api = api @@ -136,8 +138,7 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): @property def unique_id(self) -> str: """Return the system descriptor.""" - entry: ConfigEntry = self.config_entry - assert entry + entry = self.config_entry if entry.unique_id: return entry.unique_id assert entry.entry_id @@ -186,7 +187,6 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): super()._unschedule_refresh() self._async_notify_stop() - @callback async def _async_update_data(self): """Fetch the latest data from the source.""" try: diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 9785aaf54a3..39f1c78b070 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -23,7 +23,7 @@ from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, async def _validate_input( hass: core.HomeAssistant, host: str, api_version: int -) -> tuple[dict, PhilipsTV]: +) -> PhilipsTV: """Validate the user input allows us to connect.""" hub = PhilipsTV(host, api_version) @@ -44,7 +44,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" super().__init__() - self._current = {} + self._current: dict[str, Any] = {} self._hub: PhilipsTV | None = None self._pair_state: Any = None @@ -62,7 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Attempt to pair with device.""" assert self._hub - errors = {} + errors: dict[str, str] = {} schema = vol.Schema( { vol.Required(CONF_PIN): str, diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index 69a5932576f..eca3158fb15 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -65,6 +65,10 @@ async def async_attach_trigger( } device = registry.async_get(config[CONF_DEVICE_ID]) + if device is None: + raise HomeAssistantError( + f"Device id {config[CONF_DEVICE_ID]} not found in registry" + ) for config_entry_id in device.config_entries: coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN].get( config_entry_id diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 5a32d2954a4..4fa3b066214 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -137,6 +137,8 @@ class PhilipsTVLightEntity( ): """Representation of a Philips TV exposing the JointSpace API.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -146,12 +148,12 @@ class PhilipsTVLightEntity( self._hs = None self._brightness = None self._cache_keys = None - self._last_selected_effect: AmbilightEffect = None + self._last_selected_effect: AmbilightEffect | None = None super().__init__(coordinator) self._attr_supported_color_modes = {ColorMode.HS, ColorMode.ONOFF} self._attr_supported_features = LightEntityFeature.EFFECT - self._attr_name = f"{coordinator.system['name']} Ambilight" + self._attr_name = "Ambilight" self._attr_unique_id = coordinator.unique_id self._attr_icon = "mdi:television-ambient-light" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 27cf41abd4f..8d864436ac5 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -73,6 +73,7 @@ class PhilipsTVMediaPlayer( """Representation of a Philips TV exposing the JointSpace API.""" _attr_device_class = MediaPlayerDeviceClass.TV + _attr_has_entity_name = True def __init__( self, @@ -80,11 +81,9 @@ class PhilipsTVMediaPlayer( ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api - self._sources = {} - self._channels = {} + self._sources: dict[str, str] = {} self._supports = SUPPORT_PHILIPS_JS self._system = coordinator.system - self._attr_name = coordinator.system["name"] self._attr_unique_id = coordinator.unique_id self._attr_device_info = DeviceInfo( identifiers={ diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 38851964427..7e8c5448cca 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -30,6 +30,8 @@ async def async_setup_entry( class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteEntity): """Device that sends commands.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -37,7 +39,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE """Initialize the Philips TV.""" super().__init__(coordinator) self._tv = coordinator.api - self._attr_name = f"{coordinator.system['name']} Remote" + self._attr_name = "Remote" self._attr_unique_id = coordinator.unique_id self._attr_device_info = DeviceInfo( identifiers={ diff --git a/homeassistant/components/philips_js/switch.py b/homeassistant/components/philips_js/switch.py index bf1e83b0ec1..b66fd3296d9 100644 --- a/homeassistant/components/philips_js/switch.py +++ b/homeassistant/components/philips_js/switch.py @@ -38,6 +38,8 @@ class PhilipsTVScreenSwitch( ): """A Philips TV screen state switch.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -46,7 +48,7 @@ class PhilipsTVScreenSwitch( super().__init__(coordinator) - self._attr_name = f"{coordinator.system['name']} Screen State" + self._attr_name = "Screen state" self._attr_icon = "mdi:television-shimmer" self._attr_unique_id = f"{coordinator.unique_id}_screenstate" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/philips_js/translations/pt.json b/homeassistant/components/philips_js/translations/pt.json index 4646fcae7dc..f6815d0a16a 100644 --- a/homeassistant/components/philips_js/translations/pt.json +++ b/homeassistant/components/philips_js/translations/pt.json @@ -1,13 +1,20 @@ { "config": { "error": { - "invalid_pin": "PIN inv\u00e1lido" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_pin": "PIN inv\u00e1lido", + "unknown": "Erro inesperado" }, "step": { "pair": { "data": { "pin": "C\u00f3digo PIN" } + }, + "user": { + "data": { + "host": "Servidor" + } } } } diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index df156353b88..ffb3352e282 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -135,7 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_KEY_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, _async_platforms(entry)) + await hass.config_entries.async_forward_entry_setups(entry, _async_platforms(entry)) return True diff --git a/homeassistant/components/pi_hole/translations/pt.json b/homeassistant/components/pi_hole/translations/pt.json index ce1b6a07d2d..e75629c42e0 100644 --- a/homeassistant/components/pi_hole/translations/pt.json +++ b/homeassistant/components/pi_hole/translations/pt.json @@ -7,6 +7,11 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "api_key": { + "data": { + "api_key": "Chave da API" + } + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py index e34223a4799..a7d26ceb5c6 100644 --- a/homeassistant/components/picnic/__init__.py +++ b/homeassistant/components/picnic/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_COORDINATOR: picnic_coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/picnic/translations/bg.json b/homeassistant/components/picnic/translations/bg.json index 32ea4287182..aaf9f767fff 100644 --- a/homeassistant/components/picnic/translations/bg.json +++ b/homeassistant/components/picnic/translations/bg.json @@ -1,9 +1,11 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json index fd9fe67db73..b4f73a91406 100644 --- a/homeassistant/components/picnic/translations/ja.json +++ b/homeassistant/components/picnic/translations/ja.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "different_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a\u3067\u4f7f\u7528\u3057\u305f\u3082\u306e\u3068\u540c\u3058\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "different_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u7d71\u5408\u306e\u8a2d\u5b9a\u3067\u4f7f\u7528\u3057\u305f\u3082\u306e\u3068\u540c\u3058\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/picnic/translations/pt.json b/homeassistant/components/picnic/translations/pt.json new file mode 100644 index 00000000000..79bc83a3f69 --- /dev/null +++ b/homeassistant/components/picnic/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 38d7a3e18ea..0386d267f04 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -1,11 +1,16 @@ """Component to create an interface to a Pilight daemon.""" +from __future__ import annotations + +from collections.abc import Callable from datetime import timedelta import functools import logging import socket import threading +from typing import Any from pilight import pilight +from typing_extensions import ParamSpec import voluptuous as vol from homeassistant.const import ( @@ -22,6 +27,8 @@ from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util +_P = ParamSpec("_P") + _LOGGER = logging.getLogger(__name__) CONF_SEND_DELAY = "send_delay" @@ -138,23 +145,23 @@ class CallRateDelayThrottle: def __init__(self, hass, delay_seconds: float) -> None: """Initialize the delay handler.""" self._delay = timedelta(seconds=max(0.0, delay_seconds)) - self._queue: list = [] + self._queue: list[Callable[[Any], None]] = [] self._active = False self._lock = threading.Lock() self._next_ts = dt_util.utcnow() self._schedule = functools.partial(track_point_in_utc_time, hass) - def limited(self, method): + def limited(self, method: Callable[_P, Any]) -> Callable[_P, None]: """Decorate to delay calls on a certain method.""" @functools.wraps(method) - def decorated(*args, **kwargs): + def decorated(*args: _P.args, **kwargs: _P.kwargs) -> None: """Delay a call.""" if self._delay.total_seconds() == 0.0: method(*args, **kwargs) return - def action(event): + def action(event: Any) -> None: """Wrap an action that gets scheduled.""" method(*args, **kwargs) diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 19c93533867..914d92040d2 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: await async_setup_coordinator(hass, entry) - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, [platform for platform in PLATFORMS if entry.options.get(platform, True)] ) diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json index 245b0dc2a0c..990ecc7a561 100644 --- a/homeassistant/components/plaato/translations/hu.json +++ b/homeassistant/components/plaato/translations/hu.json @@ -20,7 +20,7 @@ "token": "Auth Token beilleszt\u00e9se ide", "use_webhook": "Webhook haszn\u00e1lata" }, - "description": "Az API lek\u00e9rdez\u00e9s\u00e9hez egy `auth_token` sz\u00fcks\u00e9ges, amelyet az [ezek] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) utas\u00edt\u00e1sok k\u00f6vet\u00e9s\u00e9vel lehet megszerezni. \n\n Kiv\u00e1lasztott eszk\u00f6z: ** {device_type} ** \n\nHa ink\u00e1bb a be\u00e9p\u00edtett webhook m\u00f3dszert haszn\u00e1lja (csak az Airlock eset\u00e9ben), k\u00e9rj\u00fck, jel\u00f6lje be az al\u00e1bbi n\u00e9gyzetet, \u00e9s hagyja \u00fcresen az Auth Token elemet", + "description": "Az API lek\u00e9rdez\u00e9s\u00e9hez egy `auth_token` sz\u00fcks\u00e9ges, amelyet az [ezek] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) utas\u00edt\u00e1sok k\u00f6vet\u00e9s\u00e9vel lehet megszerezni. \n\nKiv\u00e1lasztott eszk\u00f6z: **{device_type}** \n\nHa ink\u00e1bb a be\u00e9p\u00edtett webhook m\u00f3dszert haszn\u00e1lja (csak az Airlock eset\u00e9ben), k\u00e9rem, jel\u00f6lje be az al\u00e1bbi n\u00e9gyzetet, \u00e9s hagyja \u00fcresen az Auth Token elemet", "title": "API m\u00f3dszer kiv\u00e1laszt\u00e1sa" }, "user": { diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 0842ca6da74..1d7dfc19010 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json index e9890abba2f..3039abbcafb 100644 --- a/homeassistant/components/plaato/translations/pt.json +++ b/homeassistant/components/plaato/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index c8745213f90..9ff8bcf7b54 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -95,7 +95,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("Scanning for GDM clients") gdm.scan(scan_for_clients=True) - hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer( + hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer[None]( hass, _LOGGER, cooldown=10, @@ -215,7 +215,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket - def start_websocket_session(platform, _): + def start_websocket_session(platform): hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) if hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: hass.loop.create_task(websocket.listen()) @@ -228,11 +228,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + for platform in PLATFORMS: - task = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) - task.add_done_callback(partial(start_websocket_session, platform)) + start_websocket_session(platform) async_cleanup_plex_devices(hass, entry) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index ce76c4be3ff..7d21fff3afe 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -248,7 +248,7 @@ class PlexMediaPlayer(MediaPlayerEntity): else: self._attr_state = STATE_IDLE - @property + @property # type: ignore[misc] @needs_session def username(self): """Return the username of the client owner.""" @@ -279,109 +279,109 @@ class PlexMediaPlayer(MediaPlayerEntity): return "video" - @property + @property # type: ignore[misc] @needs_session def session_key(self): """Return current session key.""" return self.session.sessionKey - @property + @property # type: ignore[misc] @needs_session def media_library_title(self): """Return the library name of playing media.""" return self.session.media_library_title - @property + @property # type: ignore[misc] @needs_session def media_content_id(self): """Return the content ID of current playing media.""" return self.session.media_content_id - @property + @property # type: ignore[misc] @needs_session def media_content_type(self): """Return the content type of current playing media.""" return self.session.media_content_type - @property + @property # type: ignore[misc] @needs_session def media_content_rating(self): """Return the content rating of current playing media.""" return self.session.media_content_rating - @property + @property # type: ignore[misc] @needs_session def media_artist(self): """Return the artist of current playing media, music track only.""" return self.session.media_artist - @property + @property # type: ignore[misc] @needs_session def media_album_name(self): """Return the album name of current playing media, music track only.""" return self.session.media_album_name - @property + @property # type: ignore[misc] @needs_session def media_album_artist(self): """Return the album artist of current playing media, music only.""" return self.session.media_album_artist - @property + @property # type: ignore[misc] @needs_session def media_track(self): """Return the track number of current playing media, music only.""" return self.session.media_track - @property + @property # type: ignore[misc] @needs_session def media_duration(self): """Return the duration of current playing media in seconds.""" return self.session.media_duration - @property + @property # type: ignore[misc] @needs_session def media_position(self): """Return the duration of current playing media in seconds.""" return self.session.media_position - @property + @property # type: ignore[misc] @needs_session def media_position_updated_at(self): """When was the position of the current playing media valid.""" return self.session.media_position_updated_at - @property + @property # type: ignore[misc] @needs_session def media_image_url(self): """Return the image URL of current playing media.""" return self.session.media_image_url - @property + @property # type: ignore[misc] @needs_session def media_summary(self): """Return the summary of current playing media.""" return self.session.media_summary - @property + @property # type: ignore[misc] @needs_session def media_title(self): """Return the title of current playing media.""" return self.session.media_title - @property + @property # type: ignore[misc] @needs_session def media_season(self): """Return the season of current playing media (TV Show only).""" return self.session.media_season - @property + @property # type: ignore[misc] @needs_session def media_series_title(self): """Return the title of the series of current playing media.""" return self.session.media_series_title - @property + @property # type: ignore[misc] @needs_session def media_episode(self): """Return the episode of current playing media (TV Show only).""" @@ -515,7 +515,7 @@ class PlexMediaPlayer(MediaPlayerEntity): return attributes @property - def device_info(self) -> DeviceInfo: + def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" if self.machine_identifier is None: return None diff --git a/homeassistant/components/plex/translations/pt.json b/homeassistant/components/plex/translations/pt.json index 3b63ab169e2..6daae889e80 100644 --- a/homeassistant/components/plex/translations/pt.json +++ b/homeassistant/components/plex/translations/pt.json @@ -4,7 +4,7 @@ "already_configured": "Este servidor Plex j\u00e1 est\u00e1 configurado", "already_in_progress": "Plex est\u00e1 a ser configurado", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", - "unknown": "Falha por motivo desconhecido" + "unknown": "Erro inesperado" }, "error": { "faulty_credentials": "A autoriza\u00e7\u00e3o falhou, verifique o token" diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 129f4faef92..6f751b82b35 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -31,14 +31,14 @@ class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription): BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( PlugwiseBinarySensorEntityDescription( key="dhw_state", - name="DHW State", + name="DHW state", icon="mdi:water-pump", icon_off="mdi:water-pump-off", entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( key="flame_state", - name="Flame State", + name="Flame state", icon="mdi:fire", icon_off="mdi:fire-off", entity_category=EntityCategory.DIAGNOSTIC, @@ -59,14 +59,14 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( ), PlugwiseBinarySensorEntityDescription( key="slave_boiler_state", - name="Secondary Boiler State", + name="Secondary boiler state", icon="mdi:fire", icon_off="mdi:circle-off-outline", entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( key="plugwise_notification", - name="Plugwise Notification", + name="Plugwise notification", icon="mdi:mailbox-up-outline", icon_off="mdi:mailbox-outline", entity_category=EntityCategory.DIAGNOSTIC, @@ -118,7 +118,6 @@ class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index c74a1baa9be..9729ad745fd 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -39,6 +39,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """Representation of an Plugwise thermostat.""" _attr_temperature_unit = TEMP_CELSIUS + _attr_has_entity_name = True def __init__( self, @@ -49,7 +50,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): super().__init__(coordinator, device_id) self._attr_extra_state_attributes = {} self._attr_unique_id = f"{device_id}-climate" - self._attr_name = self.device.get("name") # Determine preset modes self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index fbfdca00b41..3fee1445758 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -119,6 +119,32 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): _version = _properties.get("version", "n/a") _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" + # This is an Anna, but we already have config entries. + # Assuming that the user has already configured Adam, aborting discovery. + if self._async_current_entries() and _product == "smile_thermo": + return self.async_abort(reason="anna_with_adam") + + # If we have discovered an Adam or Anna, both might be on the network. + # In that case, we need to cancel the Anna flow, as the Adam should + # be added. + for flow in self._async_in_progress(): + # This is an Anna, and there is already an Adam flow in progress + if ( + _product == "smile_thermo" + and "context" in flow + and flow["context"].get("product") == "smile_open_therm" + ): + return self.async_abort(reason="anna_with_adam") + + # This is an Adam, and there is already an Anna flow in progress + if ( + _product == "smile_open_therm" + and "context" in flow + and flow["context"].get("product") == "smile_thermo" + and "flow_id" in flow + ): + self.hass.config_entries.flow.async_abort(flow["flow_id"]) + self.context.update( { "title_placeholders": { @@ -128,6 +154,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): CONF_USERNAME: self._username, }, "configuration_url": f"http://{discovery_info.host}:{discovery_info.port}", + "product": _product, } ) return await self.async_step_user() diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 63bd2a6d8f1..d56d9c06ff5 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -25,8 +25,9 @@ UNIT_LUMEN: Final = "lm" PLATFORMS_GATEWAY: Final[list[str]] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, - Platform.SENSOR, + Platform.NUMBER, Platform.SELECT, + Platform.SENSOR, Platform.SWITCH, ] ZEROCONF_MAP: Final[dict[str, str]] = { diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index 491eb7c7db8..694f6e5817c 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -18,6 +18,8 @@ from .coordinator import PlugwiseDataUpdateCoordinator class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): """Represent a PlugWise Entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PlugwiseDataUpdateCoordinator, @@ -44,7 +46,7 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): connections=connections, manufacturer=data.get("vendor"), model=data.get("model"), - name=f"Smile {coordinator.data.gateway['smile_name']}", + name=coordinator.data.gateway["smile_name"], sw_version=data.get("firmware"), hw_version=data.get("hardware"), ) diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index afa7451021e..4fde6a54a4a 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -77,7 +77,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=api.smile_version[0], ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS_GATEWAY) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS_GATEWAY) return True diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index a311151f645..bf7fc453f89 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.18.5"], + "requirements": ["plugwise==0.18.7"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py new file mode 100644 index 00000000000..380be9111ba --- /dev/null +++ b/homeassistant/components/plugwise/number.py @@ -0,0 +1,114 @@ +"""Number platform for Plugwise integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from plugwise import Smile + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity + + +@dataclass +class PlugwiseEntityDescriptionMixin: + """Mixin values for Plugwse entities.""" + + command: Callable[[Smile, float], Awaitable[None]] + + +@dataclass +class PlugwiseNumberEntityDescription( + NumberEntityDescription, PlugwiseEntityDescriptionMixin +): + """Class describing Plugwise Number entities.""" + + +NUMBER_TYPES = ( + PlugwiseNumberEntityDescription( + key="maximum_boiler_temperature", + command=lambda api, value: api.set_max_boiler_temperature(value), + device_class=NumberDeviceClass.TEMPERATURE, + name="Maximum boiler temperature setpoint", + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=TEMP_CELSIUS, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Plugwise number platform.""" + + coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + entities: list[PlugwiseNumberEntity] = [] + for device_id, device in coordinator.data.devices.items(): + for description in NUMBER_TYPES: + if description.key in device: + entities.append( + PlugwiseNumberEntity(coordinator, device_id, description) + ) + + async_add_entities(entities) + + +class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): + """Representation of a Plugwise number.""" + + entity_description: PlugwiseNumberEntityDescription + + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + description: PlugwiseNumberEntityDescription, + ) -> None: + """Initiate Plugwise Number.""" + super().__init__(coordinator, device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}-{description.key}" + self._attr_mode = NumberMode.BOX + + @property + def native_step(self) -> float: + """Return the setpoint step value.""" + return max(self.device["resolution"], 1) + + @property + def native_value(self) -> float: + """Return the present setpoint value.""" + return self.device[self.entity_description.key] + + @property + def native_min_value(self) -> float: + """Return the setpoint min. value.""" + return self.device["lower_bound"] + + @property + def native_max_value(self) -> float: + """Return the setpoint max. value.""" + return self.device["upper_bound"] + + async def async_set_native_value(self, value: float) -> None: + """Change to the new setpoint value.""" + await self.entity_description.command(self.coordinator.api, value) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index cac9c3e5637..7afe76e1a8a 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -38,7 +38,7 @@ class PlugwiseSelectEntityDescription( SELECT_TYPES = ( PlugwiseSelectEntityDescription( key="select_schedule", - name="Thermostat Schedule", + name="Thermostat schedule", icon="mdi:calendar-clock", command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON), current_option="selected_schedule", @@ -46,7 +46,7 @@ SELECT_TYPES = ( ), PlugwiseSelectEntityDescription( key="select_regulation_mode", - name="Regulation Mode", + name="Regulation mode", icon="mdi:hvac", entity_category=EntityCategory.CONFIG, command=lambda api, loc, opt: api.set_regulation_mode(opt), @@ -92,7 +92,6 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = (f"{self.device['name']} {entity_description.name}").lstrip() @property def current_option(self) -> str: diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 87c81699d10..3ee0437e8dd 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -41,182 +41,182 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="intended_boiler_temperature", - name="Intended Boiler Temperature", + name="Intended boiler temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="temperature_difference", - name="Temperature Difference", + name="Temperature difference", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="outdoor_temperature", - name="Outdoor Temperature", + name="Outdoor temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="outdoor_air_temperature", - name="Outdoor Air Temperature", + name="Outdoor air temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="water_temperature", - name="Water Temperature", + name="Water temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="return_temperature", - name="Return Temperature", + name="Return temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed", - name="Electricity Consumed", + name="Electricity consumed", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced", - name="Electricity Produced", + name="Electricity produced", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_interval", - name="Electricity Consumed Interval", + name="Electricity consumed interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_peak_interval", - name="Electricity Consumed Peak Interval", + name="Electricity consumed peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_off_peak_interval", - name="Electricity Consumed Off Peak Interval", + name="Electricity consumed off peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_interval", - name="Electricity Produced Interval", + name="Electricity produced interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_peak_interval", - name="Electricity Produced Peak Interval", + name="Electricity produced peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_off_peak_interval", - name="Electricity Produced Off Peak Interval", + name="Electricity produced off peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_off_peak_point", - name="Electricity Consumed Off Peak Point", + name="Electricity consumed off peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_peak_point", - name="Electricity Consumed Peak Point", + name="Electricity consumed peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_off_peak_cumulative", - name="Electricity Consumed Off Peak Cumulative", + name="Electricity consumed off peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_consumed_peak_cumulative", - name="Electricity Consumed Peak Cumulative", + name="Electricity consumed peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_produced_off_peak_point", - name="Electricity Produced Off Peak Point", + name="Electricity produced off peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced_peak_point", - name="Electricity Produced Peak Point", + name="Electricity produced peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced_off_peak_cumulative", - name="Electricity Produced Off Peak Cumulative", + name="Electricity produced off peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_produced_peak_cumulative", - name="Electricity Produced Peak Cumulative", + name="Electricity produced peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="gas_consumed_interval", - name="Gas Consumed Interval", + name="Gas consumed interval", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="gas_consumed_cumulative", - name="Gas Consumed Cumulative", + name="Gas consumed cumulative", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="net_electricity_point", - name="Net Electricity Point", + name="Net electricity point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="net_electricity_cumulative", - name="Net Electricity Cumulative", + name="Net electricity cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, @@ -237,28 +237,28 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="modulation_level", - name="Modulation Level", + name="Modulation level", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="valve_position", - name="Valve Position", + name="Valve position", icon="mdi:valve", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="water_pressure", - name="Water Pressure", + name="Water pressure", native_unit_of_measurement=PRESSURE_BAR, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="humidity", - name="Relative Humidity", + name="Relative humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, @@ -307,7 +307,6 @@ class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property def native_value(self) -> int | float | None: diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 42dcee96196..7278f6c4414 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -19,7 +19,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna" } } } diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index 45a10297ed5..c2942308b75 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -21,7 +21,7 @@ from .util import plugwise_command SWITCHES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="dhw_cm_switch", - name="DHW Comfort Mode", + name="DHW comfort mode", icon="mdi:water-plus", entity_category=EntityCategory.CONFIG, ), @@ -68,7 +68,6 @@ class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index 1bf9efee843..ccd3d344f39 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "anna_with_adam": "Anna i Adam detectats. Afegeix l'Adam en lloc de l'Anna" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 2b9d112977a..fb80fecef25 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert", + "anna_with_adam": "Sowohl Anna als auch Adam entdeckt. F\u00fcge deinen Adam anstelle deiner Anna hinzu" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index caee7bb5a88..18a50e86b66 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "anna_with_adam": "\u03a4\u03cc\u03c3\u03bf \u03b7 \u0386\u03bd\u03bd\u03b1 \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03bf \u0391\u03b4\u03ac\u03bc \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd. \u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u0391\u03b4\u03ac\u03bc \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u0386\u03bd\u03bd\u03b1 \u03c3\u03b1\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 3f365bfa25e..cd10502d0c3 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 2d50be06193..9f2f2e0b1b6 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Teenus on juba seadistatud" + "already_configured": "Teenus on juba seadistatud", + "anna_with_adam": "Nii Anna kui ka Adam on tuvastatud. Lisa oma Anna asemel oma Adam" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 0306437b405..85f4f652c18 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "anna_with_adam": "Anna et Adam ont tous deux \u00e9t\u00e9 d\u00e9tect\u00e9s. Ajoutez votre Adam au lieu de votre Anna" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index b622109797c..c97911bad09 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "anna_with_adam": "Anna \u00e9s \u00c1d\u00e1m is \u00e9szlelve lett. Adja hozz\u00e1 az \u00c1d\u00e1mot az Anna helyett" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -29,7 +30,7 @@ "port": "Port", "username": "Smile Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg", + "description": "K\u00e9rem, adja meg", "title": "Csatlakoz\u00e1s a Smile-hoz" } } diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index 22c871e4f83..daa2824df27 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "anna_with_adam": "Baik Anna dan Adam terdeteksi. Tambahkan Adam, bukan Anna" }, "error": { "cannot_connect": "Gagal terhubung", diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 24e75f3e846..e4bf239b7bc 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "anna_with_adam": "Sia Anna che Adam sono stati rilevati. Aggiungi il tuo Adamo al posto della tua Anna" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index 87b8d501e0d..15eb388d2c8 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "anna_with_adam": "\u30a2\u30f3\u30ca\u3068\u30a2\u30c0\u30e0\u306e\u4e21\u65b9\u3092\u691c\u51fa\u3057\u307e\u3057\u305f\u3002\u30a2\u30f3\u30ca\u306e\u4ee3\u308f\u308a\u306b\u30a2\u30c0\u30e0\u3092\u8ffd\u52a0" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "invalid_setup": "Anna\u306e\u4ee3\u308f\u308a\u306b\u3001Adam\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant Plugwise\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_setup": "Anna\u306e\u4ee3\u308f\u308a\u306b\u3001Adam\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant Plugwise\u7d71\u5408\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 3d6a3a3b354..8de8da8e4e9 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "anna_with_adam": "Wykryto zar\u00f3wno Ann\u0119, jak i Adama. Dodaj Adama zamiast Anny." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/plugwise/translations/pt-BR.json b/homeassistant/components/plugwise/translations/pt-BR.json index 12f8070f074..5f667f8d6ff 100644 --- a/homeassistant/components/plugwise/translations/pt-BR.json +++ b/homeassistant/components/plugwise/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "anna_with_adam": "Tanto Anna quanto Adam detectaram. Adicione seu Adam em vez de sua Anna" }, "error": { "cannot_connect": "Falha ao conectar", diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 62b7c1e27d3..a6a31b7b63a 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "anna_with_adam": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0410\u043d\u043d\u0430 \u0438 \u0410\u0434\u0430\u043c. \u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0410\u0434\u0430\u043c\u0430 \u0432\u043c\u0435\u0441\u0442\u043e \u0410\u043d\u043d\u044b." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -13,10 +14,14 @@ "step": { "user": { "data": { - "flow_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + "flow_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "Smile ID", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Smile" }, - "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:", - "title": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Plugwise" + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index b4b49a5e52d..41f52761dbf 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "anna_with_adam": "Anna ve Adam tespit edildi. Anna'n\u0131z\u0131n yerine Adam'\u0131n\u0131z\u0131 ekleyin" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -19,8 +20,8 @@ "port": "Port", "username": "Smile Kullan\u0131c\u0131 Ad\u0131" }, - "description": "\u00dcr\u00fcn:", - "title": "Plugwise tipi" + "description": "L\u00fctfen girin", + "title": "Smile'a Ba\u011flan\u0131n" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index 9d37a79462b..2ea2c4b09ba 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "anna_with_adam": "\u767c\u73fe Anna \u8207 Adam\uff0c\u662f\u5426\u8981\u65b0\u589e Adam \u800c\u975e Anna" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 83aec34a185..aa82d5662c6 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = plum - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def cleanup(event): """Clean up resources.""" diff --git a/homeassistant/components/point/translations/ja.json b/homeassistant/components/point/translations/ja.json index 6d573895877..884d6d9a1db 100644 --- a/homeassistant/components/point/translations/ja.json +++ b/homeassistant/components/point/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "external_setup": "\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u30dd\u30a4\u30f3\u30c8\u304c\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002", "no_flows": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/point/translations/pt.json b/homeassistant/components/point/translations/pt.json index 3af92508762..fdb8b0c2c8f 100644 --- a/homeassistant/components/point/translations/pt.json +++ b/homeassistant/components/point/translations/pt.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "S\u00f3 pode configurar uma \u00fanica conta Point.", + "already_setup": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/).", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { - "default": "Autenticado com sucesso com Minut para o(s) seu(s) dispositivo (s) Point" + "default": "Autenticado com sucesso" }, "error": { "follow_link": "Por favor, siga o link e autentique antes de pressionar Enviar", diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 72bfee387eb..a3e9a93da37 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 31e249ec806..2fdf3d61d20 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -174,7 +174,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = runtime_data - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/powerwall/translations/bg.json b/homeassistant/components/powerwall/translations/bg.json index 12186a54eec..f0092b14bc1 100644 --- a/homeassistant/components/powerwall/translations/bg.json +++ b/homeassistant/components/powerwall/translations/bg.json @@ -3,6 +3,7 @@ "abort": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({ip_address})?" diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index 6f53b1ef575..2f59abef956 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -9,7 +9,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "wrong_version": "Az powerwall nem t\u00e1mogatott szoftververzi\u00f3t haszn\u00e1l. K\u00e9rj\u00fck, fontolja meg a probl\u00e9ma friss\u00edt\u00e9s\u00e9t vagy jelent\u00e9s\u00e9t, hogy megoldhat\u00f3 legyen." + "wrong_version": "Az powerwall nem t\u00e1mogatott szoftververzi\u00f3t haszn\u00e1l. K\u00e9rem, fontolja meg a probl\u00e9ma friss\u00edt\u00e9s\u00e9t vagy jelent\u00e9s\u00e9t, hogy megoldhat\u00f3 legyen." }, "flow_title": "{name} ({ip_address})", "step": { diff --git a/homeassistant/components/powerwall/translations/pt.json b/homeassistant/components/powerwall/translations/pt.json index c748619963b..1ded97b1b27 100644 --- a/homeassistant/components/powerwall/translations/pt.json +++ b/homeassistant/components/powerwall/translations/pt.json @@ -8,9 +8,15 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confim": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { - "ip_address": "Endere\u00e7o IP" + "ip_address": "Endere\u00e7o IP", + "password": "Palavra-passe" } } } diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 978c19dc2ab..69bbd39a77a 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -7,7 +7,7 @@ import sys import threading import time import traceback -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -123,10 +123,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for thread in threading.enumerate(): if thread == main_thread: continue + ident = cast(int, thread.ident) _LOGGER.critical( "Thread [%s]: %s", thread.name, - "".join(traceback.format_stack(frames.get(thread.ident))).strip(), + "".join(traceback.format_stack(frames.get(ident))).strip(), ) async def _async_dump_scheduled(call: ServiceCall) -> None: @@ -136,13 +137,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: original_maxother = arepr.maxother arepr.maxstring = 300 arepr.maxother = 300 + handle: asyncio.Handle try: - for handle in hass.loop._scheduled: # pylint: disable=protected-access + for handle in getattr(hass.loop, "_scheduled"): if not handle.cancelled(): _LOGGER.critical("Scheduled: %s", handle) finally: - arepr.max_string = original_maxstring - arepr.max_other = original_maxother + arepr.maxstring = original_maxstring + arepr.maxother = original_maxother async_register_admin_service( hass, diff --git a/homeassistant/components/profiler/translations/ja.json b/homeassistant/components/profiler/translations/ja.json index c9c3cc04633..acdaf12a90c 100644 --- a/homeassistant/components/profiler/translations/ja.json +++ b/homeassistant/components/profiler/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index 1ebabd5bb08..bce25c07b17 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Check board validation again to load new values to API. await hass.data[DOMAIN][entry.entry_id].check_board() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py index b8023c7fadd..04f353e96b8 100644 --- a/homeassistant/components/prosegur/__init__.py +++ b/homeassistant/components/prosegur/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Could not connect with Prosegur backend: %s", error) raise ConfigEntryNotReady from error - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/prosegur/translations/pt.json b/homeassistant/components/prosegur/translations/pt.json index d479d880d7f..a4b08e68352 100644 --- a/homeassistant/components/prosegur/translations/pt.json +++ b/homeassistant/components/prosegur/translations/pt.json @@ -8,6 +8,11 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index d12f5ed92fd..59d19bc785e 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==9.1.1"], + "requirements": ["pillow==9.2.0"], "codeowners": [] } diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 7e215060d73..e480396e6a2 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -75,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up PS4 from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ps4/translations/pt.json b/homeassistant/components/ps4/translations/pt.json index cf428838b0b..ac585a23994 100644 --- a/homeassistant/components/ps4/translations/pt.json +++ b/homeassistant/components/ps4/translations/pt.json @@ -15,7 +15,7 @@ "step": { "link": { "data": { - "code": "PIN", + "code": "C\u00f3digo PIN", "ip_address": "Endere\u00e7o de IP", "name": "Nome", "region": "Regi\u00e3o" diff --git a/homeassistant/components/pure_energie/__init__.py b/homeassistant/components/pure_energie/__init__.py index 4e86726ccc8..4a64e5abb84 100644 --- a/homeassistant/components/pure_energie/__init__.py +++ b/homeassistant/components/pure_energie/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pure_energie/translations/pt.json b/homeassistant/components/pure_energie/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvoutput/__init__.py b/homeassistant/components/pvoutput/__init__.py index 6457a4d25c2..bca5a23e62b 100644 --- a/homeassistant/components/pvoutput/__init__.py +++ b/homeassistant/components/pvoutput/__init__.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 7bd4b8789eb..a0afee0f3eb 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -47,7 +47,7 @@ class PVOutputSensorEntityDescription( SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( PVOutputSensorEntityDescription( key="energy_consumption", - name="Energy Consumed", + name="Energy consumed", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -55,7 +55,7 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="energy_generation", - name="Energy Generated", + name="Energy generated", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -70,7 +70,7 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="power_consumption", - name="Power Consumed", + name="Power consumed", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -78,7 +78,7 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="power_generation", - name="Power Generated", + name="Power generated", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -129,6 +129,7 @@ class PVOutputSensorEntity( """Representation of a PVOutput sensor.""" entity_description: PVOutputSensorEntityDescription + _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/pvoutput/translations/pt.json b/homeassistant/components/pvoutput/translations/pt.json new file mode 100644 index 00000000000..98fe3611480 --- /dev/null +++ b/homeassistant/components/pvoutput/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 7ecc89020c0..f10d3b995a8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json index d252c078a2c..79ce20e5033 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pt.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "power": "Pot\u00eancia contratada (kW)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index 6d8f2b83bc3..e754de466d1 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -1,15 +1,19 @@ """Support for Queensland Bushfire Alert Feeds.""" from __future__ import annotations +from collections.abc import Callable from datetime import timedelta import logging +from typing import Any -from georss_qld_bushfire_alert_client import QldBushfireAlertFeedManager +from georss_qld_bushfire_alert_client import ( + QldBushfireAlertFeedEntry, + QldBushfireAlertFeedManager, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, @@ -17,7 +21,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -70,19 +74,19 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Queensland Bushfire Alert Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - categories = config[CONF_CATEGORIES] + radius_in_km: float = config[CONF_RADIUS] + categories: list[str] = config[CONF_CATEGORIES] # Initialize the entity manager. feed = QldBushfireFeedEntityManager( hass, add_entities, scan_interval, coordinates, radius_in_km, categories ) - def start_feed_manager(event): + def start_feed_manager(event: Event) -> None: """Start feed manager.""" feed.startup() @@ -93,8 +97,14 @@ class QldBushfireFeedEntityManager: """Feed Entity Manager for Qld Bushfire Alert GeoRSS feed.""" def __init__( - self, hass, add_entities, scan_interval, coordinates, radius_in_km, categories - ): + self, + hass: HomeAssistant, + add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + radius_in_km: float, + categories: list[str], + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass self._feed_manager = QldBushfireAlertFeedManager( @@ -108,32 +118,32 @@ class QldBushfireFeedEntityManager: self._add_entities = add_entities self._scan_interval = scan_interval - def startup(self): + def startup(self) -> None: """Start up this manager.""" self._feed_manager.update() self._init_regular_updates() - def _init_regular_updates(self): + def _init_regular_updates(self) -> None: """Schedule regular updates at the specified interval.""" track_time_interval( self._hass, lambda now: self._feed_manager.update(), self._scan_interval ) - def get_entry(self, external_id): + def get_entry(self, external_id: str) -> QldBushfireAlertFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = QldBushfireLocationEvent(self, external_id) # Add new entities to HA. self._add_entities([new_entity], True) - def _update_entity(self, external_id): + def _update_entity(self, external_id: str) -> None: """Update entity.""" dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - def _remove_entity(self, external_id): + def _remove_entity(self, external_id: str) -> None: """Remove entity.""" dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) @@ -141,23 +151,25 @@ class QldBushfireFeedEntityManager: class QldBushfireLocationEvent(GeolocationEvent): """This represents an external event with Qld Bushfire feed data.""" - def __init__(self, feed_manager, external_id): + _attr_icon = "mdi:fire" + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__( + self, feed_manager: QldBushfireFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._category = None self._publication_date = None self._updated_date = None self._status = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -171,84 +183,43 @@ class QldBushfireLocationEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for Qld Bushfire Alert feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: QldBushfireAlertFeedEntry) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._category = feed_entry.category self._publication_date = feed_entry.published self._updated_date = feed_entry.updated self._status = feed_entry.status @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:fire" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), (ATTR_CATEGORY, self._category), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_PUBLICATION_DATE, self._publication_date), (ATTR_UPDATED_DATE, self._updated_date), (ATTR_STATUS, self._status), diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index 26ed8066686..b3bcc1705de 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -1,17 +1,27 @@ """The QNAP QSW integration.""" from __future__ import annotations +import logging + from aioqsw.localapi import ConnectionOptions, QnapQswApi from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from .const import DOMAIN -from .coordinator import QswUpdateCoordinator +from .const import DOMAIN, QSW_COORD_DATA, QSW_COORD_FW +from .coordinator import QswDataCoordinator, QswFirmwareCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.SENSOR, + Platform.UPDATE, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -24,12 +34,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: qsw = QnapQswApi(aiohttp_client.async_get_clientsession(hass), options) - coordinator = QswUpdateCoordinator(hass, qsw) - await coordinator.async_config_entry_first_refresh() + coord_data = QswDataCoordinator(hass, qsw) + await coord_data.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + coord_fw = QswFirmwareCoordinator(hass, qsw) + try: + await coord_fw.async_config_entry_first_refresh() + except ConfigEntryNotReady as error: + _LOGGER.warning(error) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + QSW_COORD_DATA: coord_data, + QSW_COORD_FW: coord_fw, + } + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/qnap_qsw/binary_sensor.py b/homeassistant/components/qnap_qsw/binary_sensor.py index 467a3314070..71af89778b8 100644 --- a/homeassistant/components/qnap_qsw/binary_sensor.py +++ b/homeassistant/components/qnap_qsw/binary_sensor.py @@ -16,8 +16,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_MESSAGE, DOMAIN -from .coordinator import QswUpdateCoordinator +from .const import ATTR_MESSAGE, DOMAIN, QSW_COORD_DATA +from .coordinator import QswDataCoordinator from .entity import QswEntityDescription, QswSensorEntity @@ -48,7 +48,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW binary sensors from a config_entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] async_add_entities( QswBinarySensor(coordinator, description, entry) for description in BINARY_SENSOR_TYPES @@ -66,7 +66,7 @@ class QswBinarySensor(QswSensorEntity, BinarySensorEntity): def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, description: QswBinarySensorEntityDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/qnap_qsw/button.py b/homeassistant/components/qnap_qsw/button.py index 1c13310fe05..9aad411b992 100644 --- a/homeassistant/components/qnap_qsw/button.py +++ b/homeassistant/components/qnap_qsw/button.py @@ -17,9 +17,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, QSW_REBOOT -from .coordinator import QswUpdateCoordinator -from .entity import QswEntity +from .const import DOMAIN, QSW_COORD_DATA, QSW_REBOOT +from .coordinator import QswDataCoordinator +from .entity import QswDataEntity @dataclass @@ -49,20 +49,20 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW buttons from a config_entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] async_add_entities( QswButton(coordinator, description, entry) for description in BUTTON_TYPES ) -class QswButton(QswEntity, ButtonEntity): +class QswButton(QswDataEntity, ButtonEntity): """Define a QNAP QSW button.""" entity_description: QswButtonDescription def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, description: QswButtonDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index bb42c9ea294..794e8c67baa 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -78,7 +78,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("DHCP discovery detected QSW: %s", self._discovered_mac) - mac = format_mac(self._discovered_mac) options = ConnectionOptions(self._discovered_url, "", "") qsw = QnapQswApi(aiohttp_client.async_get_clientsession(self.hass), options) @@ -87,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except QswError as err: raise AbortFlow("cannot_connect") from err - await self.async_set_unique_id(format_mac(mac)) + await self.async_set_unique_id(format_mac(self._discovered_mac)) self._abort_if_unique_id_configured() return await self.async_step_discovered_connection() diff --git a/homeassistant/components/qnap_qsw/const.py b/homeassistant/components/qnap_qsw/const.py index e583c0250f4..4b5fa9a4a2c 100644 --- a/homeassistant/components/qnap_qsw/const.py +++ b/homeassistant/components/qnap_qsw/const.py @@ -10,5 +10,8 @@ MANUFACTURER: Final = "QNAP" RPM: Final = "rpm" +QSW_COORD_DATA: Final = "coordinator-data" +QSW_COORD_FW: Final = "coordinator-firmware" QSW_REBOOT = "reboot" QSW_TIMEOUT_SEC: Final = 25 +QSW_UPDATE: Final = "update" diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index c018c1f3848..eb4e60bf9bd 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -14,12 +14,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, QSW_TIMEOUT_SEC -SCAN_INTERVAL = timedelta(seconds=60) +DATA_SCAN_INTERVAL = timedelta(seconds=60) +FW_SCAN_INTERVAL = timedelta(hours=12) _LOGGER = logging.getLogger(__name__) -class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): +class QswDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the QNAP QSW device.""" def __init__(self, hass: HomeAssistant, qsw: QnapQswApi) -> None: @@ -30,7 +31,7 @@ class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): hass, _LOGGER, name=DOMAIN, - update_interval=SCAN_INTERVAL, + update_interval=DATA_SCAN_INTERVAL, ) async def _async_update_data(self) -> dict[str, Any]: @@ -41,3 +42,27 @@ class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except QswError as error: raise UpdateFailed(error) from error return self.qsw.data() + + +class QswFirmwareCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching firmware data from the QNAP QSW device.""" + + def __init__(self, hass: HomeAssistant, qsw: QnapQswApi) -> None: + """Initialize.""" + self.qsw = qsw + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=FW_SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update firmware data via library.""" + async with async_timeout.timeout(QSW_TIMEOUT_SEC): + try: + await self.qsw.check_firmware() + except QswError as error: + raise UpdateFailed(error) from error + return self.qsw.data() diff --git a/homeassistant/components/qnap_qsw/diagnostics.py b/homeassistant/components/qnap_qsw/diagnostics.py index 3730bab24a8..2467e9181a3 100644 --- a/homeassistant/components/qnap_qsw/diagnostics.py +++ b/homeassistant/components/qnap_qsw/diagnostics.py @@ -10,8 +10,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import QswUpdateCoordinator +from .const import DOMAIN, QSW_COORD_DATA, QSW_COORD_FW +from .coordinator import QswDataCoordinator, QswFirmwareCoordinator TO_REDACT_CONFIG = [ CONF_USERNAME, @@ -29,9 +29,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + entry_data = hass.data[DOMAIN][config_entry.entry_id] + coord_data: QswDataCoordinator = entry_data[QSW_COORD_DATA] + coord_fw: QswFirmwareCoordinator = entry_data[QSW_COORD_FW] return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT_CONFIG), - "coord_data": async_redact_data(coordinator.data, TO_REDACT_DATA), + "coord_data": async_redact_data(coord_data.data, TO_REDACT_DATA), + "coord_fw": async_redact_data(coord_fw.data, TO_REDACT_DATA), } diff --git a/homeassistant/components/qnap_qsw/entity.py b/homeassistant/components/qnap_qsw/entity.py index c3550610d83..7da47f9734f 100644 --- a/homeassistant/components/qnap_qsw/entity.py +++ b/homeassistant/components/qnap_qsw/entity.py @@ -20,15 +20,15 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import MANUFACTURER -from .coordinator import QswUpdateCoordinator +from .coordinator import QswDataCoordinator, QswFirmwareCoordinator -class QswEntity(CoordinatorEntity[QswUpdateCoordinator]): +class QswDataEntity(CoordinatorEntity[QswDataCoordinator]): """Define an QNAP QSW entity.""" def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, entry: ConfigEntry, ) -> None: """Initialize.""" @@ -72,7 +72,7 @@ class QswEntityDescription(EntityDescription, QswEntityDescriptionMixin): attributes: dict[str, list[str]] | None = None -class QswSensorEntity(QswEntity): +class QswSensorEntity(QswDataEntity): """Base class for QSW sensor entities.""" entity_description: QswEntityDescription @@ -91,3 +91,38 @@ class QswSensorEntity(QswEntity): key: self.get_device_value(val[0], val[1]) for key, val in self.entity_description.attributes.items() } + + +class QswFirmwareEntity(CoordinatorEntity[QswFirmwareCoordinator]): + """Define a QNAP QSW firmware entity.""" + + def __init__( + self, + coordinator: QswFirmwareCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_device_info = DeviceInfo( + configuration_url=entry.data[CONF_URL], + connections={ + ( + CONNECTION_NETWORK_MAC, + self.get_device_value(QSD_SYSTEM_BOARD, QSD_MAC), + ) + }, + manufacturer=MANUFACTURER, + model=self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT), + name=self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT), + sw_version=self.get_device_value(QSD_FIRMWARE_INFO, QSD_FIRMWARE), + ) + + def get_device_value(self, key: str, subkey: str) -> Any: + """Return device value by key.""" + value = None + if key in self.coordinator.data: + data = self.coordinator.data[key] + if subkey in data: + value = data[subkey] + return value diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index 0de8ec4a39e..618c20b4cc2 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -26,8 +26,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_MAX, DOMAIN, RPM -from .coordinator import QswUpdateCoordinator +from .const import ATTR_MAX, DOMAIN, QSW_COORD_DATA, RPM +from .coordinator import QswDataCoordinator from .entity import QswEntityDescription, QswSensorEntity @@ -82,7 +82,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW sensors from a config_entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] async_add_entities( QswSensor(coordinator, description, entry) for description in SENSOR_TYPES @@ -100,7 +100,7 @@ class QswSensor(QswSensorEntity, SensorEntity): def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, description: QswSensorEntityDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/qnap_qsw/translations/ar.json b/homeassistant/components/qnap_qsw/translations/ar.json new file mode 100644 index 00000000000..07980293874 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/ar.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "discovered_connection": { + "data": { + "password": "\u0627\u0644\u0631\u0642\u0645 \u0627\u0644\u0633\u0631\u064a", + "username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/ca.json b/homeassistant/components/qnap_qsw/translations/ca.json index 575ed369b7b..6864ced54e2 100644 --- a/homeassistant/components/qnap_qsw/translations/ca.json +++ b/homeassistant/components/qnap_qsw/translations/ca.json @@ -9,6 +9,12 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { + "discovered_connection": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/qnap_qsw/translations/cs.json b/homeassistant/components/qnap_qsw/translations/cs.json new file mode 100644 index 00000000000..33006d6761b --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/de.json b/homeassistant/components/qnap_qsw/translations/de.json index ad2599e24c4..a2b89d77ea7 100644 --- a/homeassistant/components/qnap_qsw/translations/de.json +++ b/homeassistant/components/qnap_qsw/translations/de.json @@ -9,6 +9,12 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "discovered_connection": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/qnap_qsw/translations/el.json b/homeassistant/components/qnap_qsw/translations/el.json index 1bea5dcc9d7..5aeea0a6d22 100644 --- a/homeassistant/components/qnap_qsw/translations/el.json +++ b/homeassistant/components/qnap_qsw/translations/el.json @@ -9,6 +9,12 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "step": { + "discovered_connection": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/qnap_qsw/translations/et.json b/homeassistant/components/qnap_qsw/translations/et.json index eb7ddb6dbe7..7b43f0b00f8 100644 --- a/homeassistant/components/qnap_qsw/translations/et.json +++ b/homeassistant/components/qnap_qsw/translations/et.json @@ -9,6 +9,12 @@ "invalid_auth": "Tuvastamine nurjus" }, "step": { + "discovered_connection": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/qnap_qsw/translations/fr.json b/homeassistant/components/qnap_qsw/translations/fr.json index 2631bcdfc87..2c885fee397 100644 --- a/homeassistant/components/qnap_qsw/translations/fr.json +++ b/homeassistant/components/qnap_qsw/translations/fr.json @@ -9,6 +9,12 @@ "invalid_auth": "Authentification non valide" }, "step": { + "discovered_connection": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/qnap_qsw/translations/hu.json b/homeassistant/components/qnap_qsw/translations/hu.json index c41ba084d95..60eb41375c1 100644 --- a/homeassistant/components/qnap_qsw/translations/hu.json +++ b/homeassistant/components/qnap_qsw/translations/hu.json @@ -9,6 +9,12 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "discovered_connection": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/qnap_qsw/translations/id.json b/homeassistant/components/qnap_qsw/translations/id.json index b34bcd046d9..b9f124a765c 100644 --- a/homeassistant/components/qnap_qsw/translations/id.json +++ b/homeassistant/components/qnap_qsw/translations/id.json @@ -9,6 +9,12 @@ "invalid_auth": "Autentikasi tidak valid" }, "step": { + "discovered_connection": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/qnap_qsw/translations/it.json b/homeassistant/components/qnap_qsw/translations/it.json index 0759ef4ca8a..557e937217b 100644 --- a/homeassistant/components/qnap_qsw/translations/it.json +++ b/homeassistant/components/qnap_qsw/translations/it.json @@ -9,6 +9,12 @@ "invalid_auth": "Autenticazione non valida" }, "step": { + "discovered_connection": { + "data": { + "password": "Password", + "username": "Nome utente" + } + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/qnap_qsw/translations/ja.json b/homeassistant/components/qnap_qsw/translations/ja.json index 0658405fd3f..09b93bc58c5 100644 --- a/homeassistant/components/qnap_qsw/translations/ja.json +++ b/homeassistant/components/qnap_qsw/translations/ja.json @@ -9,6 +9,12 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { + "discovered_connection": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/qnap_qsw/translations/ko.json b/homeassistant/components/qnap_qsw/translations/ko.json new file mode 100644 index 00000000000..d31f3ea7fcd --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "discovered_connection": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/nl.json b/homeassistant/components/qnap_qsw/translations/nl.json index 10ec94f589d..73e0af1fe00 100644 --- a/homeassistant/components/qnap_qsw/translations/nl.json +++ b/homeassistant/components/qnap_qsw/translations/nl.json @@ -9,6 +9,12 @@ "invalid_auth": "Ongeldige authenticatie" }, "step": { + "discovered_connection": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/qnap_qsw/translations/no.json b/homeassistant/components/qnap_qsw/translations/no.json index 9eff9c22080..97f04b406a5 100644 --- a/homeassistant/components/qnap_qsw/translations/no.json +++ b/homeassistant/components/qnap_qsw/translations/no.json @@ -9,6 +9,12 @@ "invalid_auth": "Ugyldig godkjenning" }, "step": { + "discovered_connection": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/qnap_qsw/translations/pl.json b/homeassistant/components/qnap_qsw/translations/pl.json index e90d527a7a7..f33acb2c0d6 100644 --- a/homeassistant/components/qnap_qsw/translations/pl.json +++ b/homeassistant/components/qnap_qsw/translations/pl.json @@ -9,6 +9,12 @@ "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { + "discovered_connection": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/qnap_qsw/translations/pt-BR.json b/homeassistant/components/qnap_qsw/translations/pt-BR.json index b9829d78944..fe2016bdce5 100644 --- a/homeassistant/components/qnap_qsw/translations/pt-BR.json +++ b/homeassistant/components/qnap_qsw/translations/pt-BR.json @@ -9,6 +9,12 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "discovered_connection": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + }, "user": { "data": { "password": "Senha", diff --git a/homeassistant/components/qnap_qsw/translations/pt.json b/homeassistant/components/qnap_qsw/translations/pt.json new file mode 100644 index 00000000000..9f02a9e2519 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "discovered_connection": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/ru.json b/homeassistant/components/qnap_qsw/translations/ru.json index ae3869552d0..b45585ca3a4 100644 --- a/homeassistant/components/qnap_qsw/translations/ru.json +++ b/homeassistant/components/qnap_qsw/translations/ru.json @@ -9,6 +9,12 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { + "discovered_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/qnap_qsw/translations/tr.json b/homeassistant/components/qnap_qsw/translations/tr.json index 309f2da3a90..ce11f88deca 100644 --- a/homeassistant/components/qnap_qsw/translations/tr.json +++ b/homeassistant/components/qnap_qsw/translations/tr.json @@ -9,6 +9,12 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "discovered_connection": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/qnap_qsw/translations/zh-Hant.json b/homeassistant/components/qnap_qsw/translations/zh-Hant.json index f29c42d6eb6..1461a38c7d4 100644 --- a/homeassistant/components/qnap_qsw/translations/zh-Hant.json +++ b/homeassistant/components/qnap_qsw/translations/zh-Hant.json @@ -9,6 +9,12 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { + "discovered_connection": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/qnap_qsw/update.py b/homeassistant/components/qnap_qsw/update.py new file mode 100644 index 00000000000..8dfd985ffef --- /dev/null +++ b/homeassistant/components/qnap_qsw/update.py @@ -0,0 +1,89 @@ +"""Support for the QNAP QSW update.""" +from __future__ import annotations + +from typing import Final + +from aioqsw.const import ( + QSD_DESCRIPTION, + QSD_FIRMWARE_CHECK, + QSD_FIRMWARE_INFO, + QSD_PRODUCT, + QSD_SYSTEM_BOARD, + QSD_VERSION, +) + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, QSW_COORD_FW, QSW_UPDATE +from .coordinator import QswFirmwareCoordinator +from .entity import QswFirmwareEntity + +UPDATE_TYPES: Final[tuple[UpdateEntityDescription, ...]] = ( + UpdateEntityDescription( + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + key=QSW_UPDATE, + name="Firmware Update", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add QNAP QSW updates from a config_entry.""" + coordinator: QswFirmwareCoordinator = hass.data[DOMAIN][entry.entry_id][ + QSW_COORD_FW + ] + async_add_entities( + QswUpdate(coordinator, description, entry) for description in UPDATE_TYPES + ) + + +class QswUpdate(QswFirmwareEntity, UpdateEntity): + """Define a QNAP QSW update.""" + + entity_description: UpdateEntityDescription + + def __init__( + self, + coordinator: QswFirmwareCoordinator, + description: UpdateEntityDescription, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry) + self._attr_name = ( + f"{self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT)} {description.name}" + ) + self._attr_unique_id = f"{entry.unique_id}_{description.key}" + self.entity_description = description + + self._attr_installed_version = self.get_device_value( + QSD_FIRMWARE_INFO, QSD_VERSION + ) + self._async_update_attrs() + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update attributes.""" + self._attr_latest_version = self.get_device_value( + QSD_FIRMWARE_CHECK, QSD_VERSION + ) + self._attr_release_summary = self.get_device_value( + QSD_FIRMWARE_CHECK, QSD_DESCRIPTION + ) diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 6715d1ba6db..666cc0c93ce 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==9.1.1", "pyzbar==0.1.7"], + "requirements": ["pillow==9.2.0", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated", "loggers": ["pyzbar"] diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index 5ce387dc9f3..49f674fdec6 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -2,7 +2,7 @@ "domain": "quantum_gateway", "name": "Quantum Gateway", "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", - "requirements": ["quantum-gateway==0.0.6"], + "requirements": ["quantum-gateway==0.0.8"], "codeowners": ["@cisasteelersfan"], "iot_class": "local_polling" } diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index e0ac98b7546..6723678012f 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -95,6 +95,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person async_register_webhook(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/rachio/translations/pt.json b/homeassistant/components/rachio/translations/pt.json index 8a4fdd22177..d6320473ac0 100644 --- a/homeassistant/components/rachio/translations/pt.json +++ b/homeassistant/components/rachio/translations/pt.json @@ -15,5 +15,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Dura\u00e7\u00e3o em minutos para funcionar ao activar um interruptor de zona" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/radio_browser/__init__.py b/homeassistant/components/radio_browser/__init__.py index 89c2f220159..d93d7c48823 100644 --- a/homeassistant/components/radio_browser/__init__.py +++ b/homeassistant/components/radio_browser/__init__.py @@ -15,7 +15,7 @@ from .const import DOMAIN async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Radio Browser from a config entry. - This integration doesn't set up any enitites, as it provides a media source + This integration doesn't set up any entities, as it provides a media source only. """ session = async_get_clientsession(hass) diff --git a/homeassistant/components/radio_browser/translations/ja.json b/homeassistant/components/radio_browser/translations/ja.json index 24b32e6e30a..0143961d6ce 100644 --- a/homeassistant/components/radio_browser/translations/ja.json +++ b/homeassistant/components/radio_browser/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/radio_browser/translations/pt.json b/homeassistant/components/radio_browser/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index 865e75257ec..787570eaeb4 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_call_or_raise_not_ready(time_coro, host) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index 3f8e87e74a4..c466a7108e8 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -18,6 +18,7 @@ from homeassistant.components.climate.const import ( HVACAction, HVACMode, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, @@ -125,13 +126,22 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Radio Thermostat.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( - # config flow added in 2022.7 and should be removed in 2022.9 "Configuration of the Radio Thermostat climate platform in YAML is deprecated and " "will be removed in Home Assistant 2022.9; Your existing configuration " "has been imported into the UI automatically and can be safely removed " "from your configuration.yaml file" ) + hosts: list[str] = [] if CONF_HOST in config: hosts = config[CONF_HOST] diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index c6ae4e5bb06..5c37b4e3cde 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -3,6 +3,7 @@ "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": ["radiotherm==2.1.0"], + "dependencies": ["repairs"], "codeowners": ["@bdraco", "@vinnyfuria"], "iot_class": "local_polling", "loggers": ["radiotherm"], diff --git a/homeassistant/components/radiotherm/strings.json b/homeassistant/components/radiotherm/strings.json index 22f17224285..51505d4d727 100644 --- a/homeassistant/components/radiotherm/strings.json +++ b/homeassistant/components/radiotherm/strings.json @@ -19,6 +19,12 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "issues": { + "deprecated_yaml": { + "title": "The Radio Thermostat YAML configuration is being removed", + "description": "Configuring the Radio Thermostat climate platform using YAML is being removed in Home Assistant 2022.9.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/ar.json b/homeassistant/components/radiotherm/translations/ar.json new file mode 100644 index 00000000000..217c499dd98 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ca.json b/homeassistant/components/radiotherm/translations/ca.json index d1249ddd23f..58e8487607f 100644 --- a/homeassistant/components/radiotherm/translations/ca.json +++ b/homeassistant/components/radiotherm/translations/ca.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de la plataforma 'Radio Thermostat' mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "S'est\u00e0 eliminant la configuraci\u00f3 YAML de Thermostat Radio" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/de.json b/homeassistant/components/radiotherm/translations/de.json index 50315afce52..2845d3d6190 100644 --- a/homeassistant/components/radiotherm/translations/de.json +++ b/homeassistant/components/radiotherm/translations/de.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration der Radiothermostat-Klimaplattform mit YAML wird in Home Assistant 2022.9 entfernt. \n\nDeine vorhandene Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die YAML-Konfiguration des Funkthermostats wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/el.json b/homeassistant/components/radiotherm/translations/el.json index 9b276da3670..8e60a12d1e8 100644 --- a/homeassistant/components/radiotherm/translations/el.json +++ b/homeassistant/components/radiotherm/translations/el.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03bb\u03b9\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae\u03c2 \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 \u03c1\u03b1\u03b4\u03b9\u03bf\u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c1\u03b1\u03b4\u03b9\u03bf\u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/en.json b/homeassistant/components/radiotherm/translations/en.json index b524f188e59..224c8ffeb3c 100644 --- a/homeassistant/components/radiotherm/translations/en.json +++ b/homeassistant/components/radiotherm/translations/en.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Radio Thermostat climate platform using YAML is being removed in Home Assistant 2022.9.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Radio Thermostat YAML configuration is being removed" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/et.json b/homeassistant/components/radiotherm/translations/et.json index f8a6a7303ab..8eefd4125bb 100644 --- a/homeassistant/components/radiotherm/translations/et.json +++ b/homeassistant/components/radiotherm/translations/et.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Raadiotermostaadi kliimaplatvormi konfigureerimine YAML-i abil eemaldatakse rakenduses Home Assistant 2022.9. \n\n Olemasolev konfiguratsioon imporditi kasutajaliidesesse automaatselt. Selle probleemi lahendamiseks eemalda YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivita Home Assistant.", + "title": "Raadiotermostaadi YAML-i konfiguratsioon eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index 653dd56321b..fef1c64746b 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Radio Thermostat verr\u00e0 rimossa" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/pl.json b/homeassistant/components/radiotherm/translations/pl.json index e69568131d6..4265577b46e 100644 --- a/homeassistant/components/radiotherm/translations/pl.json +++ b/homeassistant/components/radiotherm/translations/pl.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja platformy klimatycznej Radio Thermostat za pomoc\u0105 YAML zostanie usuni\u0119ta w Home Assistant 2022.9. \n\nTwoja istniej\u0105ca konfiguracja zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Radio Thermostat zostanie usuni\u0119ta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/pt-BR.json b/homeassistant/components/radiotherm/translations/pt-BR.json index da10f6bd457..de9b3f092cc 100644 --- a/homeassistant/components/radiotherm/translations/pt-BR.json +++ b/homeassistant/components/radiotherm/translations/pt-BR.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o da plataforma clim\u00e1tica Radio Thermostat usando YAML est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Sua configura\u00e7\u00e3o existente foi importada para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do r\u00e1dio termostato est\u00e1 sendo removida" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/pt.json b/homeassistant/components/radiotherm/translations/pt.json new file mode 100644 index 00000000000..ae100e45845 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ru.json b/homeassistant/components/radiotherm/translations/ru.json new file mode 100644 index 00000000000..6d4615f9820 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/ru.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0433\u043e \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f \u043f\u0440\u0438 \u0440\u0435\u0433\u0443\u043b\u0438\u0440\u043e\u0432\u043a\u0435 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/zh-Hant.json b/homeassistant/components/radiotherm/translations/zh-Hant.json index 7a5a40817f7..ad1af3bb442 100644 --- a/homeassistant/components/radiotherm/translations/zh-Hant.json +++ b/homeassistant/components/radiotherm/translations/zh-Hant.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Radio \u6eab\u63a7\u5668\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Radio \u6eab\u63a7\u5668 YAML \u8a2d\u5b9a\u5df2\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainforest_eagle/__init__.py b/homeassistant/components/rainforest_eagle/__init__.py index 862c9850cb9..7cf540de1e6 100644 --- a/homeassistant/components/rainforest_eagle/__init__.py +++ b/homeassistant/components/rainforest_eagle/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = data.EagleDataCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/rainforest_eagle/translations/pt.json b/homeassistant/components/rainforest_eagle/translations/pt.json new file mode 100644 index 00000000000..91786f4b324 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index c285bf89e57..3feeac7a827 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -255,7 +255,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinators, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) @@ -401,6 +401,8 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: class RainMachineEntity(CoordinatorEntity): """Define a generic RainMachine entity.""" + _attr_has_entity_name = True + def __init__( self, entry: ConfigEntry, @@ -415,7 +417,7 @@ class RainMachineEntity(CoordinatorEntity): identifiers={(DOMAIN, controller.mac)}, configuration_url=f"https://{entry.data[CONF_IP_ADDRESS]}:{entry.data[CONF_PORT]}", connections={(dr.CONNECTION_NETWORK_MAC, controller.mac)}, - name=str(controller.name), + name=str(controller.name).capitalize(), manufacturer="RainMachine", model=( f"Version {controller.hardware_version} " @@ -424,7 +426,6 @@ class RainMachineEntity(CoordinatorEntity): sw_version=controller.software_version, ) self._attr_extra_state_attributes = {} - self._attr_name = f"{controller.name} {description.name}" self._attr_unique_id = f"{controller.mac}_{description.key}" self._controller = controller self.entity_description = description diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 7a13515db3b..6ba374a28ba 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -43,14 +43,14 @@ class RainMachineBinarySensorDescription( BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_FLOW_SENSOR, - name="Flow Sensor", + name="Flow sensor", icon="mdi:water-pump", api_category=DATA_PROVISION_SETTINGS, data_key="useFlowSensor", ), RainMachineBinarySensorDescription( key=TYPE_FREEZE, - name="Freeze Restrictions", + name="Freeze restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, @@ -58,7 +58,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_FREEZE_PROTECTION, - name="Freeze Protection", + name="Freeze protection", icon="mdi:weather-snowy", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, @@ -66,7 +66,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_HOT_DAYS, - name="Extra Water on Hot Days", + name="Extra water on hot days", icon="mdi:thermometer-lines", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, @@ -74,7 +74,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_HOURLY, - name="Hourly Restrictions", + name="Hourly restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -83,7 +83,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_MONTH, - name="Month Restrictions", + name="Month restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -92,7 +92,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_RAINDELAY, - name="Rain Delay Restrictions", + name="Rain delay restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -101,7 +101,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_RAINSENSOR, - name="Rain Sensor Restrictions", + name="Rain sensor restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -110,7 +110,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_WEEKDAY, - name="Weekday Restrictions", + name="Weekday restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 2982d7176a6..e57386fe0ec 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -68,7 +68,7 @@ class RainMachineSensorDescriptionUid( SENSOR_DESCRIPTIONS = ( RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_CLICK_M3, - name="Flow Sensor Clicks per Cubic Meter", + name="Flow sensor clicks per cubic meter", icon="mdi:water-pump", native_unit_of_measurement=f"clicks/{VOLUME_CUBIC_METERS}", entity_category=EntityCategory.DIAGNOSTIC, @@ -79,7 +79,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, - name="Flow Sensor Consumed Liters", + name="Flow sensor consumed liters", icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="liter", @@ -90,7 +90,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_START_INDEX, - name="Flow Sensor Start Index", + name="Flow sensor start index", icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="index", @@ -100,7 +100,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, - name="Flow Sensor Clicks", + name="Flow sensor clicks", icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="clicks", @@ -111,7 +111,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FREEZE_TEMP, - name="Freeze Protect Temperature", + name="Freeze protect temperature", icon="mdi:thermometer", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=TEMP_CELSIUS, diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index 7dcbe2cd8d7..198aec94a22 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -1,32 +1,4 @@ # Describes the format for available RainMachine services -disable_program: - name: Disable Program - description: Disable a program - target: - entity: - integration: rainmachine - domain: switch -disable_zone: - name: Disable Zone - description: Disable a zone - target: - entity: - integration: rainmachine - domain: switch -enable_program: - name: Enable Program - description: Enable a program - target: - entity: - integration: rainmachine - domain: switch -enable_zone: - name: Enable Zone - description: Enable a zone - target: - entity: - integration: rainmachine - domain: switch pause_watering: name: Pause All Watering description: Pause all watering activities for a number of seconds diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 8d339682305..aa91f529b5b 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -149,6 +149,8 @@ async def async_setup_entry( ("zone", zone_coordinator, RainMachineZone, RainMachineZoneEnabled), ): for uid, data in coordinator.data.items(): + name = data["name"].capitalize() + # Add a switch to start/stop the program or zone: entities.append( switch_class( @@ -157,7 +159,7 @@ async def async_setup_entry( controller, RainMachineSwitchDescription( key=f"{kind}_{uid}", - name=data["name"], + name=name, icon="mdi:water", uid=uid, ), @@ -172,7 +174,7 @@ async def async_setup_entry( controller, RainMachineSwitchDescription( key=f"{kind}_{uid}_enabled", - name=f"{data['name']} Enabled", + name=f"{name} enabled", entity_category=EntityCategory.CONFIG, icon="mdi:cog", uid=uid, diff --git a/homeassistant/components/rdw/__init__.py b/homeassistant/components/rdw/__init__.py index ac2bed06310..601d21ee889 100644 --- a/homeassistant/components/rdw/__init__.py +++ b/homeassistant/components/rdw/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py index 81d3e448b78..25ef6b27f1d 100644 --- a/homeassistant/components/rdw/binary_sensor.py +++ b/homeassistant/components/rdw/binary_sensor.py @@ -41,13 +41,13 @@ class RDWBinarySensorEntityDescription( BINARY_SENSORS: tuple[RDWBinarySensorEntityDescription, ...] = ( RDWBinarySensorEntityDescription( key="liability_insured", - name="Liability Insured", + name="Liability insured", icon="mdi:shield-car", is_on_fn=lambda vehicle: vehicle.liability_insured, ), RDWBinarySensorEntityDescription( key="pending_recall", - name="Pending Recall", + name="Pending recall", device_class=BinarySensorDeviceClass.PROBLEM, is_on_fn=lambda vehicle: vehicle.pending_recall, ), @@ -75,6 +75,7 @@ class RDWBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): """Defines an RDW binary sensor.""" entity_description: RDWBinarySensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -91,7 +92,7 @@ class RDWBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.data.license_plate)}, manufacturer=coordinator.data.brand, - name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", + name=f"{coordinator.data.brand} {coordinator.data.license_plate}", model=coordinator.data.model, configuration_url=f"https://ovi.rdw.nl/default.aspx?kenteken={coordinator.data.license_plate}", ) diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index 04f525c61b8..d4cb97005a8 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -42,13 +42,13 @@ class RDWSensorEntityDescription( SENSORS: tuple[RDWSensorEntityDescription, ...] = ( RDWSensorEntityDescription( key="apk_expiration", - name="APK Expiration", + name="APK expiration", device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.apk_expiration, ), RDWSensorEntityDescription( key="ascription_date", - name="Ascription Date", + name="Ascription date", device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.ascription_date, ), @@ -76,6 +76,7 @@ class RDWSensorEntity(CoordinatorEntity, SensorEntity): """Defines an RDW sensor.""" entity_description: RDWSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -93,7 +94,7 @@ class RDWSensorEntity(CoordinatorEntity, SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{license_plate}")}, manufacturer=coordinator.data.brand, - name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", + name=f"{coordinator.data.brand} {coordinator.data.license_plate}", model=coordinator.data.model, configuration_url=f"https://ovi.rdw.nl/default.aspx?kenteken={coordinator.data.license_plate}", ) diff --git a/homeassistant/components/rdw/translations/pt.json b/homeassistant/components/rdw/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/rdw/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index a977ce18f3f..c4c3f9a1e35 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index fab482a00e6..7d527ac56c6 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -28,11 +28,11 @@ SENSOR_TYPE_NEXT_PICKUP = "next_pickup" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_CURRENT_PICKUP, - name="Current Pickup", + name="Current pickup", ), SensorEntityDescription( key=SENSOR_TYPE_NEXT_PICKUP, - name="Next Pickup", + name="Next pickup", ), ) @@ -68,6 +68,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """ReCollect Waste Sensor.""" _attr_device_class = SensorDeviceClass.DATE + _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 4063e443e8b..f9ed5f59333 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -31,6 +31,7 @@ from .const import ( from .core import Recorder from .services import async_register_services from .tasks import AddRecorderPlatformTask +from .util import get_instance _LOGGER = logging.getLogger(__name__) @@ -108,12 +109,6 @@ CONFIG_SCHEMA = vol.Schema( ) -def get_instance(hass: HomeAssistant) -> Recorder: - """Get the recorder instance.""" - instance: Recorder = hass.data[DATA_INSTANCE] - return instance - - @bind_hass def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """Check if an entity is being recorded. @@ -122,13 +117,12 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """ if DATA_INSTANCE not in hass.data: return False - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) return instance.entity_filter(entity_id) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" - hass.data[DOMAIN] = {} exclude_attributes_by_domain: dict[str, set[str]] = {} hass.data[EXCLUDE_ATTRIBUTES] = exclude_attributes_by_domain conf = config[DOMAIN] @@ -177,5 +171,5 @@ async def _process_recorder_platform( hass: HomeAssistant, domain: str, platform: Any ) -> None: """Process a recorder platform.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) instance.queue_task(AddRecorderPlatformTask(domain, platform)) diff --git a/homeassistant/components/recorder/backup.py b/homeassistant/components/recorder/backup.py index cec9f85748b..a1f6f4f39bc 100644 --- a/homeassistant/components/recorder/backup.py +++ b/homeassistant/components/recorder/backup.py @@ -4,9 +4,7 @@ from logging import getLogger from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import Recorder -from .const import DATA_INSTANCE -from .util import async_migration_in_progress +from .util import async_migration_in_progress, get_instance _LOGGER = getLogger(__name__) @@ -14,7 +12,7 @@ _LOGGER = getLogger(__name__) async def async_pre_backup(hass: HomeAssistant) -> None: """Perform operations before a backup starts.""" _LOGGER.info("Backup start notification, locking database for writes") - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) if async_migration_in_progress(hass): raise HomeAssistantError("Database migration in progress") await instance.lock_database() @@ -22,7 +20,7 @@ async def async_pre_backup(hass: HomeAssistant) -> None: async def async_post_backup(hass: HomeAssistant) -> None: """Perform operations after a backup finishes.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) _LOGGER.info("Backup end notification, releasing write lock") if not instance.unlock_database(): raise HomeAssistantError("Could not release database write lock") diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 9585804690a..30ece9e98a5 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -43,6 +43,7 @@ import homeassistant.util.dt as dt_util from . import migration, statistics from .const import ( DB_WORKER_PREFIX, + DOMAIN, KEEPALIVE_TIME, MAX_QUEUE_BACKLOG, MYSQLDB_URL_PREFIX, @@ -74,7 +75,7 @@ from .tasks import ( CommitTask, DatabaseLockTask, EventTask, - ExternalStatisticsTask, + ImportStatisticsTask, KeepAliveTask, PerodicCleanupTask, PurgeTask, @@ -166,7 +167,12 @@ class Recorder(threading.Thread): self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait self.engine_version: AwesomeVersion | None = None + # Database connection is ready, but non-live migration may be in progress + db_connected: asyncio.Future[bool] = hass.data[DOMAIN].db_connected + self.async_db_connected: asyncio.Future[bool] = db_connected + # Database is ready to use but live migration may be in progress self.async_db_ready: asyncio.Future[bool] = asyncio.Future() + # Database is ready to use and all migration steps completed (used by tests) self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() self.engine: Engine | None = None @@ -188,6 +194,7 @@ class Recorder(threading.Thread): self._completed_first_database_setup: bool | None = None self.async_migration_event = asyncio.Event() self.migration_in_progress = False + self.migration_is_live = False self._database_lock_task: DatabaseLockTask | None = None self._db_executor: DBInterruptibleThreadPoolExecutor | None = None self._exclude_attributes_by_domain = exclude_attributes_by_domain @@ -289,15 +296,16 @@ class Recorder(threading.Thread): def _stop_executor(self) -> None: """Stop the executor.""" - assert self._db_executor is not None + if self._db_executor is None: + return self._db_executor.shutdown() self._db_executor = None @callback def _async_check_queue(self, *_: Any) -> None: - """Periodic check of the queue size to ensure we do not exaust memory. + """Periodic check of the queue size to ensure we do not exhaust memory. - The queue grows during migraton or if something really goes wrong. + The queue grows during migration or if something really goes wrong. """ size = self.backlog _LOGGER.debug("Recorder queue size is: %s", size) @@ -410,6 +418,7 @@ class Recorder(threading.Thread): @callback def async_connection_failed(self) -> None: """Connect failed tasks.""" + self.async_db_connected.set_result(False) self.async_db_ready.set_result(False) persistent_notification.async_create( self.hass, @@ -420,13 +429,29 @@ class Recorder(threading.Thread): @callback def async_connection_success(self) -> None: - """Connect success tasks.""" + """Connect to the database succeeded, schema version and migration need known. + + The database may not yet be ready for use in case of a non-live migration. + """ + self.async_db_connected.set_result(True) + + @callback + def async_set_db_ready(self) -> None: + """Database live and ready for use. + + Called after non-live migration steps are finished. + """ + if self.async_db_ready.done(): + return self.async_db_ready.set_result(True) self.async_start_executor() @callback - def _async_recorder_ready(self) -> None: - """Finish start and mark recorder ready.""" + def _async_set_recorder_ready_migration_done(self) -> None: + """Finish start and mark recorder ready. + + Called after all migration steps are finished. + """ self._async_setup_periodic_tasks() self.async_recorder_ready.set() @@ -480,11 +505,11 @@ class Recorder(threading.Thread): ) @callback - def async_external_statistics( + def async_import_statistics( self, metadata: StatisticMetaData, stats: Iterable[StatisticData] ) -> None: - """Schedule external statistics.""" - self.queue_task(ExternalStatisticsTask(metadata, stats)) + """Schedule import of statistics.""" + self.queue_task(ImportStatisticsTask(metadata, stats)) @callback def _async_setup_periodic_tasks(self) -> None: @@ -548,16 +573,23 @@ class Recorder(threading.Thread): self._setup_run() else: self.migration_in_progress = True + self.migration_is_live = migration.live_migration(current_version) self.hass.add_job(self.async_connection_success) - # If shutdown happened before Home Assistant finished starting - if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: - self.migration_in_progress = False - # Make sure we cleanly close the run if - # we restart before startup finishes - self._shutdown() - return + if self.migration_is_live or schema_is_current: + # If the migrate is live or the schema is current, we need to + # wait for startup to complete. If its not live, we need to continue + # on. + self.hass.add_job(self.async_set_db_ready) + # If shutdown happened before Home Assistant finished starting + if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: + self.migration_in_progress = False + # Make sure we cleanly close the run if + # we restart before startup finishes + self._shutdown() + self.hass.add_job(self.async_set_db_ready) + return # We wait to start the migration until startup has finished # since it can be cpu intensive and we do not want it to compete @@ -577,11 +609,14 @@ class Recorder(threading.Thread): "Database Migration Failed", "recorder_database_migration", ) + self.hass.add_job(self.async_set_db_ready) self._shutdown() return + self.hass.add_job(self.async_set_db_ready) + _LOGGER.debug("Recorder processing the queue") - self.hass.add_job(self._async_recorder_ready) + self.hass.add_job(self._async_set_recorder_ready_migration_done) self._run_event_loop() def _run_event_loop(self) -> None: @@ -659,7 +694,7 @@ class Recorder(threading.Thread): try: migration.migrate_schema( - self.hass, self.engine, self.get_session, current_version + self, self.hass, self.engine, self.get_session, current_version ) except exc.DatabaseError as err: if self._handle_database_error(err): diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 36487353e25..4777eeb500e 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta import logging -from typing import Any, cast +from typing import Any, TypeVar, cast import ciso8601 from fnvhash import fnv1a_32 @@ -55,6 +55,8 @@ Base = declarative_base() SCHEMA_VERSION = 29 +_StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") + _LOGGER = logging.getLogger(__name__) TABLE_EVENTS = "events" @@ -443,7 +445,9 @@ class StatisticsBase: sum = Column(DOUBLE_TYPE) @classmethod - def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: + def from_stats( + cls: type[_StatisticsBaseSelfT], metadata_id: int, stats: StatisticData + ) -> _StatisticsBaseSelfT: """Create object from a statistics.""" return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 3d22781906a..f0c1f81689a 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0", "lru-dict==1.1.7"], + "requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 7e11e62502d..6e4a67c9da5 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Iterable import contextlib from datetime import timedelta import logging -from typing import cast +from typing import Any, cast import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text @@ -40,6 +40,8 @@ from .statistics import ( ) from .util import session_scope +LIVE_MIGRATION_MIN_SCHEMA_VERSION = 0 + _LOGGER = logging.getLogger(__name__) @@ -78,7 +80,13 @@ def schema_is_current(current_version: int) -> bool: return current_version == SCHEMA_VERSION +def live_migration(current_version: int) -> bool: + """Check if live migration is possible.""" + return current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION + + def migrate_schema( + instance: Any, hass: HomeAssistant, engine: Engine, session_maker: Callable[[], Session], @@ -86,7 +94,12 @@ def migrate_schema( ) -> None: """Check if the schema needs to be upgraded.""" _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) + db_ready = False for version in range(current_version, SCHEMA_VERSION): + if live_migration(version) and not db_ready: + db_ready = True + instance.migration_is_live = True + hass.add_job(instance.async_set_db_ready) new_version = version + 1 _LOGGER.info("Upgrading recorder db schema to version %s", new_version) _apply_update(hass, engine, session_maker, new_version, current_version) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ff53d9be3d1..98c9fc7c9b2 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,6 +1,8 @@ """Models for Recorder.""" from __future__ import annotations +import asyncio +from dataclasses import dataclass, field from datetime import datetime import logging from typing import Any, TypedDict, overload @@ -30,6 +32,14 @@ class UnsupportedDialect(Exception): """The dialect or its version is not supported.""" +@dataclass +class RecorderData: + """Recorder data stored in hass.data.""" + + recorder_platforms: dict[str, Any] = field(default_factory=dict) + db_connected: asyncio.Future = field(default_factory=asyncio.Future) + + class StatisticResult(TypedDict): """Statistic result data class. diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 26221aa199b..fcd8e4f3930 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -29,7 +29,7 @@ from homeassistant.const import ( VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder @@ -41,7 +41,7 @@ import homeassistant.util.temperature as temperature_util from homeassistant.util.unit_system import UnitSystem import homeassistant.util.volume as volume_util -from .const import DATA_INSTANCE, DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect +from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect from .db_schema import Statistics, StatisticsMeta, StatisticsRuns, StatisticsShortTerm from .models import ( StatisticData, @@ -53,6 +53,7 @@ from .models import ( from .util import ( execute, execute_stmt_lambda_element, + get_instance, retryable_database_job, session_scope, ) @@ -209,7 +210,7 @@ def async_setup(hass: HomeAssistant) -> None: @callback def _async_entity_id_changed(event: Event) -> None: - hass.data[DATA_INSTANCE].async_update_statistics_metadata( + get_instance(hass).async_update_statistics_metadata( event.data["old_entity_id"], new_statistic_id=event.data["entity_id"] ) @@ -575,7 +576,7 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: platform_stats: list[StatisticResult] = [] current_metadata: dict[str, tuple[int, StatisticMetaData]] = {} # Collect statistics from all platforms implementing support - for domain, platform in instance.hass.data[DOMAIN].items(): + for domain, platform in instance.hass.data[DOMAIN].recorder_platforms.items(): if not hasattr(platform, "compile_statistics"): continue compiled: PlatformCompiledStatistics = platform.compile_statistics( @@ -850,7 +851,7 @@ def list_statistic_ids( } # Query all integrations with a registered recorder platform - for platform in hass.data[DOMAIN].values(): + for platform in hass.data[DOMAIN].recorder_platforms.values(): if not hasattr(platform, "list_statistic_ids"): continue platform_statistic_ids = platform.list_statistic_ids( @@ -1338,7 +1339,7 @@ def _sorted_statistics_to_dict( def validate_statistics(hass: HomeAssistant) -> dict[str, list[ValidationIssue]]: """Validate statistics.""" platform_validation: dict[str, list[ValidationIssue]] = {} - for platform in hass.data[DOMAIN].values(): + for platform in hass.data[DOMAIN].recorder_platforms.values(): if not hasattr(platform, "validate_statistics"): continue platform_validation.update(platform.validate_statistics(hass)) @@ -1360,6 +1361,54 @@ def _statistics_exists( return result["id"] if result else None +@callback +def _async_import_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + statistics: Iterable[StatisticData], +) -> None: + """Validate timestamps and insert an import_statistics job in the recorder's queue.""" + for statistic in statistics: + start = statistic["start"] + if start.tzinfo is None or start.tzinfo.utcoffset(start) is None: + raise HomeAssistantError("Naive timestamp") + if start.minute != 0 or start.second != 0 or start.microsecond != 0: + raise HomeAssistantError("Invalid timestamp") + statistic["start"] = dt_util.as_utc(start) + + if "last_reset" in statistic and statistic["last_reset"] is not None: + last_reset = statistic["last_reset"] + if ( + last_reset.tzinfo is None + or last_reset.tzinfo.utcoffset(last_reset) is None + ): + raise HomeAssistantError("Naive timestamp") + statistic["last_reset"] = dt_util.as_utc(last_reset) + + # Insert job in recorder's queue + get_instance(hass).async_import_statistics(metadata, statistics) + + +@callback +def async_import_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + statistics: Iterable[StatisticData], +) -> None: + """Import hourly statistics from an internal source. + + This inserts an import_statistics job in the recorder's queue. + """ + if not valid_entity_id(metadata["statistic_id"]): + raise HomeAssistantError("Invalid statistic_id") + + # The source must not be empty and must be aligned with the statistic_id + if not metadata["source"] or metadata["source"] != DOMAIN: + raise HomeAssistantError("Invalid source") + + _async_import_statistics(hass, metadata, statistics) + + @callback def async_add_external_statistics( hass: HomeAssistant, @@ -1368,7 +1417,7 @@ def async_add_external_statistics( ) -> None: """Add hourly statistics from an external source. - This inserts an add_external_statistics job in the recorder's queue. + This inserts an import_statistics job in the recorder's queue. """ # The statistic_id has same limitations as an entity_id, but with a ':' as separator if not valid_statistic_id(metadata["statistic_id"]): @@ -1379,16 +1428,7 @@ def async_add_external_statistics( if not metadata["source"] or metadata["source"] != domain: raise HomeAssistantError("Invalid source") - for statistic in statistics: - start = statistic["start"] - if start.tzinfo is None or start.tzinfo.utcoffset(start) is None: - raise HomeAssistantError("Naive timestamp") - if start.minute != 0 or start.second != 0 or start.microsecond != 0: - raise HomeAssistantError("Invalid timestamp") - statistic["start"] = dt_util.as_utc(start) - - # Insert job in recorder's queue - hass.data[DATA_INSTANCE].async_external_statistics(metadata, statistics) + _async_import_statistics(hass, metadata, statistics) def _filter_unique_constraint_integrity_error( @@ -1432,12 +1472,12 @@ def _filter_unique_constraint_integrity_error( @retryable_database_job("statistics") -def add_external_statistics( +def import_statistics( instance: Recorder, metadata: StatisticMetaData, statistics: Iterable[StatisticData], ) -> bool: - """Process an add_external_statistics job.""" + """Process an import_statistics job.""" with session_scope( session=instance.get_session(), diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 5ec83a3cefc..cdb97d9d67c 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -124,18 +124,18 @@ class StatisticsTask(RecorderTask): @dataclass -class ExternalStatisticsTask(RecorderTask): - """An object to insert into the recorder queue to run an external statistics task.""" +class ImportStatisticsTask(RecorderTask): + """An object to insert into the recorder queue to run an import statistics task.""" metadata: StatisticMetaData statistics: Iterable[StatisticData] def run(self, instance: Recorder) -> None: """Run statistics task.""" - if statistics.add_external_statistics(instance, self.metadata, self.statistics): + if statistics.import_statistics(instance, self.metadata, self.statistics): return # Schedule a new statistics task if this one didn't finish - instance.queue_task(ExternalStatisticsTask(self.metadata, self.statistics)) + instance.queue_task(ImportStatisticsTask(self.metadata, self.statistics)) @dataclass @@ -249,7 +249,7 @@ class AddRecorderPlatformTask(RecorderTask): domain = self.domain platform = self.platform - platforms: dict[str, Any] = hass.data[DOMAIN] + platforms: dict[str, Any] = hass.data[DOMAIN].recorder_platforms platforms[domain] = platform if hasattr(self.platform, "exclude_attributes"): hass.data[EXCLUDE_ATTRIBUTES][domain] = platform.exclude_attributes(hass) diff --git a/homeassistant/components/recorder/translations/pt.json b/homeassistant/components/recorder/translations/pt.json new file mode 100644 index 00000000000..b06f3d16e60 --- /dev/null +++ b/homeassistant/components/recorder/translations/pt.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "database_engine": "Motor de Base de Dados", + "database_version": "Vers\u00e3o de Base de Dados" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c1fbc831987..ddc8747f79b 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -80,7 +80,7 @@ def session_scope( ) -> Generator[Session, None, None]: """Provide a transactional scope around a series of operations.""" if session is None and hass is not None: - session = hass.data[DATA_INSTANCE].get_session() + session = get_instance(hass).get_session() if session is None: raise RuntimeError("Session required") @@ -552,7 +552,19 @@ def write_lock_db_sqlite(instance: Recorder) -> Generator[None, None, None]: def async_migration_in_progress(hass: HomeAssistant) -> bool: - """Determine is a migration is in progress. + """Determine if a migration is in progress. + + This is a thin wrapper that allows us to change + out the implementation later. + """ + if DATA_INSTANCE not in hass.data: + return False + instance = get_instance(hass) + return instance.migration_in_progress + + +def async_migration_is_live(hass: HomeAssistant) -> bool: + """Determine if a migration is live. This is a thin wrapper that allows us to change out the implementation later. @@ -560,7 +572,7 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: if DATA_INSTANCE not in hass.data: return False instance: Recorder = hass.data[DATA_INSTANCE] - return instance.migration_in_progress + return instance.migration_is_live def second_sunday(year: int, month: int) -> date: @@ -577,3 +589,9 @@ def second_sunday(year: int, month: int) -> date: def is_second_sunday(date_time: datetime) -> bool: """Check if a time is the second sunday of the month.""" return bool(second_sunday(date_time.year, date_time.month).day == date_time.day) + + +def get_instance(hass: HomeAssistant) -> Recorder: + """Get the recorder instance.""" + instance: Recorder = hass.data[DATA_INSTANCE] + return instance diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 45e2cf5620b..16813944780 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -2,20 +2,22 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback, valid_entity_id +from homeassistant.helpers import config_validation as cv from homeassistant.util import dt as dt_util -from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG -from .statistics import list_statistic_ids, validate_statistics -from .util import async_migration_in_progress - -if TYPE_CHECKING: - from . import Recorder +from .const import MAX_QUEUE_BACKLOG +from .statistics import ( + async_add_external_statistics, + async_import_statistics, + list_statistic_ids, + validate_statistics, +) +from .util import async_migration_in_progress, async_migration_is_live, get_instance _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -31,6 +33,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_backup_start) websocket_api.async_register_command(hass, ws_backup_end) websocket_api.async_register_command(hass, ws_adjust_sum_statistics) + websocket_api.async_register_command(hass, ws_import_statistics) @websocket_api.websocket_command( @@ -43,7 +46,7 @@ async def ws_validate_statistics( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) statistic_ids = await instance.async_add_executor_job( validate_statistics, hass, @@ -67,7 +70,7 @@ def ws_clear_statistics( Note: The WS call posts a job to the recorder's queue and then returns, it doesn't wait until the job is completed. """ - hass.data[DATA_INSTANCE].async_clear_statistics(msg["statistic_ids"]) + get_instance(hass).async_clear_statistics(msg["statistic_ids"]) connection.send_result(msg["id"]) @@ -82,7 +85,7 @@ async def ws_get_statistics_metadata( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Get metadata for a list of statistic_ids.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) statistic_ids = await instance.async_add_executor_job( list_statistic_ids, hass, msg.get("statistic_ids") ) @@ -102,7 +105,7 @@ def ws_update_statistics_metadata( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Update statistics metadata for a statistic_id.""" - hass.data[DATA_INSTANCE].async_update_statistics_metadata( + get_instance(hass).async_update_statistics_metadata( msg["statistic_id"], new_unit_of_measurement=msg["unit_of_measurement"] ) connection.send_result(msg["id"]) @@ -130,12 +133,52 @@ def ws_adjust_sum_statistics( connection.send_error(msg["id"], "invalid_start_time", "Invalid start time") return - hass.data[DATA_INSTANCE].async_adjust_statistics( + get_instance(hass).async_adjust_statistics( msg["statistic_id"], start_time, msg["adjustment"] ) connection.send_result(msg["id"]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/import_statistics", + vol.Required("metadata"): { + vol.Required("has_mean"): bool, + vol.Required("has_sum"): bool, + vol.Required("name"): vol.Any(str, None), + vol.Required("source"): str, + vol.Required("statistic_id"): str, + vol.Required("unit_of_measurement"): vol.Any(str, None), + }, + vol.Required("stats"): [ + { + vol.Required("start"): cv.datetime, + vol.Optional("mean"): vol.Any(float, int), + vol.Optional("min"): vol.Any(float, int), + vol.Optional("max"): vol.Any(float, int), + vol.Optional("last_reset"): vol.Any(cv.datetime, None), + vol.Optional("state"): vol.Any(float, int), + vol.Optional("sum"): vol.Any(float, int), + } + ], + } +) +@callback +def ws_import_statistics( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Adjust sum statistics.""" + metadata = msg["metadata"] + stats = msg["stats"] + + if valid_entity_id(metadata["statistic_id"]): + async_import_statistics(hass, metadata, stats) + else: + async_add_external_statistics(hass, metadata, stats) + connection.send_result(msg["id"]) + + @websocket_api.websocket_command( { vol.Required("type"): "recorder/info", @@ -146,10 +189,11 @@ def ws_info( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Return status of the recorder.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) backlog = instance.backlog if instance else None migration_in_progress = async_migration_in_progress(hass) + migration_is_live = async_migration_is_live(hass) recording = instance.recording if instance else False thread_alive = instance.is_alive() if instance else False @@ -157,6 +201,7 @@ def ws_info( "backlog": backlog, "max_backlog": MAX_QUEUE_BACKLOG, "migration_in_progress": migration_in_progress, + "migration_is_live": migration_is_live, "recording": recording, "thread_running": thread_alive, } @@ -172,7 +217,7 @@ async def ws_backup_start( """Backup start notification.""" _LOGGER.info("Backup start notification, locking database for writes") - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) try: await instance.lock_database() except TimeoutError as err: @@ -189,7 +234,7 @@ async def ws_backup_end( ) -> None: """Backup end notification.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) _LOGGER.info("Backup end notification, releasing write lock") if not instance.unlock_database(): connection.send_error( diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index 17e4d3dd82b..21ce5118401 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][config_entry.entry_id] = renault_hub - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_AC_START): setup_services(hass) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index a24c9be4e6d..f309a8f188a 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -85,7 +85,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( key="plugged_in", coordinator="battery", device_class=BinarySensorDeviceClass.PLUG, - name="Plugged In", + name="Plugged in", on_key="plugStatus", on_value=PlugState.PLUGGED.value, ), @@ -130,7 +130,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( coordinator="lock_status", # On means open, Off means closed device_class=BinarySensorDeviceClass.DOOR, - name=f"{door} Door", + name=f"{door.capitalize()} door", on_key=f"doorStatus{door.replace(' ','')}", on_value="open", ) diff --git a/homeassistant/components/renault/button.py b/homeassistant/components/renault/button.py index e62bdf083ae..71d197ac335 100644 --- a/homeassistant/components/renault/button.py +++ b/homeassistant/components/renault/button.py @@ -71,13 +71,13 @@ BUTTON_TYPES: tuple[RenaultButtonEntityDescription, ...] = ( async_press=_start_air_conditioner, key="start_air_conditioner", icon="mdi:air-conditioner", - name="Start Air Conditioner", + name="Start air conditioner", ), RenaultButtonEntityDescription( async_press=_start_charge, key="start_charge", icon="mdi:ev-station", - name="Start Charge", + name="Start charge", requires_electricity=True, ), ) diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index 82f071decae..188d429016a 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from typing import cast -from homeassistant.const import ATTR_NAME from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -28,6 +27,7 @@ class RenaultDataEntityDescription(EntityDescription, RenaultDataRequiredKeysMix class RenaultEntity(Entity): """Implementation of a Renault entity with a data coordinator.""" + _attr_has_entity_name = True entity_description: EntityDescription def __init__( @@ -41,14 +41,6 @@ class RenaultEntity(Entity): self._attr_device_info = self.vehicle.device_info self._attr_unique_id = f"{self.vehicle.details.vin}_{description.key}".lower() - @property - def name(self) -> str: - """Return the name of the entity. - - Overridden to include the device name. - """ - return f"{self.vehicle.device_info[ATTR_NAME]} {self.entity_description.name}" - class RenaultDataEntity( CoordinatorEntity[RenaultDataUpdateCoordinator[T]], RenaultEntity diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index e7ec97b3927..9af47206e3c 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -98,7 +98,7 @@ SENSOR_TYPES: tuple[RenaultSelectEntityDescription, ...] = ( data_key="chargeMode", device_class=DEVICE_CLASS_CHARGE_MODE, icon_lambda=_get_charge_mode_icon, - name="Charge Mode", + name="Charge mode", options=["always", "always_charging", "schedule_mode"], ), ) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 3b486af175b..7e692182ff9 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -164,7 +164,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryLevel", device_class=SensorDeviceClass.BATTERY, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Battery Level", + name="Battery level", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), @@ -175,7 +175,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=DEVICE_CLASS_CHARGE_STATE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon_lambda=_get_charge_state_icon, - name="Charge State", + name="Charge state", value_lambda=_get_charge_state_formatted, ), RenaultSensorEntityDescription( @@ -184,7 +184,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingRemainingTime", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon="mdi:timer", - name="Charging Remaining Time", + name="Charging remaining time", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), @@ -195,7 +195,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingInstantaneousPower", device_class=SensorDeviceClass.CURRENT, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Charging Power", + name="Charging power", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, ), @@ -206,7 +206,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingInstantaneousPower", device_class=SensorDeviceClass.POWER, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Charging Power", + name="Charging power", native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, value_lambda=_get_charging_power, @@ -218,7 +218,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=DEVICE_CLASS_PLUG_STATE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon_lambda=_get_plug_state_icon, - name="Plug State", + name="Plug state", value_lambda=_get_plug_state_formatted, ), RenaultSensorEntityDescription( @@ -227,7 +227,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryAutonomy", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon="mdi:ev-station", - name="Battery Autonomy", + name="Battery autonomy", native_unit_of_measurement=LENGTH_KILOMETERS, state_class=SensorStateClass.MEASUREMENT, ), @@ -237,7 +237,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryAvailableEnergy", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], device_class=SensorDeviceClass.ENERGY, - name="Battery Available Energy", + name="Battery available energy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.MEASUREMENT, ), @@ -247,7 +247,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryTemperature", device_class=SensorDeviceClass.TEMPERATURE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Battery Temperature", + name="Battery temperature", native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), @@ -258,7 +258,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="timestamp", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], entity_registry_enabled_default=False, - name="Battery Last Activity", + name="Battery last activity", value_lambda=_get_utc_value, ), RenaultSensorEntityDescription( @@ -278,7 +278,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="fuelAutonomy", entity_class=RenaultSensor[KamereonVehicleCockpitData], icon="mdi:gas-station", - name="Fuel Autonomy", + name="Fuel autonomy", native_unit_of_measurement=LENGTH_KILOMETERS, state_class=SensorStateClass.MEASUREMENT, requires_fuel=True, @@ -290,7 +290,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="fuelQuantity", entity_class=RenaultSensor[KamereonVehicleCockpitData], icon="mdi:fuel", - name="Fuel Quantity", + name="Fuel quantity", native_unit_of_measurement=VOLUME_LITERS, state_class=SensorStateClass.MEASUREMENT, requires_fuel=True, @@ -302,7 +302,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=SensorDeviceClass.TEMPERATURE, data_key="externalTemperature", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], - name="Outside Temperature", + name="Outside temperature", native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), @@ -311,7 +311,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( coordinator="hvac_status", data_key="socThreshold", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], - name="HVAC SOC Threshold", + name="HVAC SoC threshold", native_unit_of_measurement=PERCENTAGE, ), RenaultSensorEntityDescription( @@ -321,7 +321,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="lastUpdateTime", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], entity_registry_enabled_default=False, - name="HVAC Last Activity", + name="HVAC last activity", value_lambda=_get_utc_value, ), RenaultSensorEntityDescription( @@ -331,7 +331,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="lastUpdateTime", entity_class=RenaultSensor[KamereonVehicleLocationData], entity_registry_enabled_default=False, - name="Location Last Activity", + name="Location last activity", value_lambda=_get_utc_value, ), RenaultSensorEntityDescription( @@ -339,7 +339,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( coordinator="res_state", data_key="details", entity_class=RenaultSensor[KamereonVehicleResStateData], - name="Remote Engine Start", + name="Remote engine start", ), RenaultSensorEntityDescription( key="res_state_code", @@ -347,6 +347,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="code", entity_class=RenaultSensor[KamereonVehicleResStateData], entity_registry_enabled_default=False, - name="Remote Engine Start Code", + name="Remote engine start code", ), ) diff --git a/homeassistant/components/renault/translations/hu.json b/homeassistant/components/renault/translations/hu.json index d74d8cdf9e4..9d2367af80b 100644 --- a/homeassistant/components/renault/translations/hu.json +++ b/homeassistant/components/renault/translations/hu.json @@ -19,7 +19,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, friss\u00edtse {username} jelszav\u00e1t", + "description": "K\u00e9rem, friss\u00edtse {username} jelszav\u00e1t", "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li hiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/renault/translations/ja.json b/homeassistant/components/renault/translations/ja.json index 743dfa36fb5..42d819589b7 100644 --- a/homeassistant/components/renault/translations/ja.json +++ b/homeassistant/components/renault/translations/ja.json @@ -20,7 +20,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/renault/translations/pt.json b/homeassistant/components/renault/translations/pt.json new file mode 100644 index 00000000000..f2223dc3827 --- /dev/null +++ b/homeassistant/components/renault/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_credentials": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py new file mode 100644 index 00000000000..4471def0dcd --- /dev/null +++ b/homeassistant/components/repairs/__init__.py @@ -0,0 +1,37 @@ +"""The repairs integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from . import issue_handler, websocket_api +from .const import DOMAIN +from .issue_handler import ( + async_create_issue, + async_delete_issue, + create_issue, + delete_issue, +) +from .issue_registry import async_load as async_load_issue_registry +from .models import IssueSeverity, RepairsFlow + +__all__ = [ + "async_create_issue", + "async_delete_issue", + "create_issue", + "delete_issue", + "DOMAIN", + "IssueSeverity", + "RepairsFlow", +] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Repairs.""" + hass.data[DOMAIN] = {} + + issue_handler.async_setup(hass) + websocket_api.async_setup(hass) + await async_load_issue_registry(hass) + + return True diff --git a/homeassistant/components/repairs/const.py b/homeassistant/components/repairs/const.py new file mode 100644 index 00000000000..cddc5edcffd --- /dev/null +++ b/homeassistant/components/repairs/const.py @@ -0,0 +1,3 @@ +"""Constants for the Repairs integration.""" + +DOMAIN = "repairs" diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py new file mode 100644 index 00000000000..c139026ec48 --- /dev/null +++ b/homeassistant/components/repairs/issue_handler.py @@ -0,0 +1,179 @@ +"""The repairs integration.""" +from __future__ import annotations + +import functools as ft +from typing import Any + +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant import data_entry_flow +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) +from homeassistant.util.async_ import run_callback_threadsafe + +from .const import DOMAIN +from .issue_registry import async_get as async_get_issue_registry +from .models import IssueSeverity, RepairsFlow, RepairsProtocol + + +class RepairsFlowManager(data_entry_flow.FlowManager): + """Manage repairs flows.""" + + async def async_create_flow( + self, + handler_key: Any, + *, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> RepairsFlow: + """Create a flow. platform is a repairs module.""" + if "platforms" not in self.hass.data[DOMAIN]: + await async_process_repairs_platforms(self.hass) + + platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"] + if handler_key not in platforms: + raise data_entry_flow.UnknownHandler + platform = platforms[handler_key] + + assert data and "issue_id" in data + issue_id = data["issue_id"] + + issue_registry = async_get_issue_registry(self.hass) + issue = issue_registry.async_get_issue(handler_key, issue_id) + if issue is None or not issue.is_fixable: + raise data_entry_flow.UnknownStep + + return await platform.async_create_fix_flow(self.hass, issue_id) + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: + """Complete a fix flow.""" + async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"]) + if "result" not in result: + result["result"] = None + return result + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Initialize repairs.""" + hass.data[DOMAIN]["flow_manager"] = RepairsFlowManager(hass) + + +async def async_process_repairs_platforms(hass: HomeAssistant) -> None: + """Start processing repairs platforms.""" + hass.data[DOMAIN]["platforms"] = {} + + await async_process_integration_platforms(hass, DOMAIN, _register_repairs_platform) + + +async def _register_repairs_platform( + hass: HomeAssistant, integration_domain: str, platform: RepairsProtocol +) -> None: + """Register a repairs platform.""" + if not hasattr(platform, "async_create_fix_flow"): + raise HomeAssistantError(f"Invalid repairs platform {platform}") + hass.data[DOMAIN]["platforms"][integration_domain] = platform + + +@callback +def async_create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + issue_domain: str | None = None, + breaks_in_ha_version: str | None = None, + is_fixable: bool, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + # Verify the breaks_in_ha_version is a valid version string + if breaks_in_ha_version: + AwesomeVersion( + breaks_in_ha_version, + ensure_strategy=AwesomeVersionStrategy.CALVER, + find_first_match=False, + ) + + issue_registry = async_get_issue_registry(hass) + issue_registry.async_get_or_create( + domain, + issue_id, + issue_domain=issue_domain, + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + +def create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + is_fixable: bool, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + return run_callback_threadsafe( + hass.loop, + ft.partial( + async_create_issue, + hass, + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ), + ).result() + + +@callback +def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + issue_registry = async_get_issue_registry(hass) + issue_registry.async_delete(domain, issue_id) + + +def delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + return run_callback_threadsafe( + hass.loop, async_delete_issue, hass, domain, issue_id + ).result() + + +@callback +def async_ignore_issue( + hass: HomeAssistant, domain: str, issue_id: str, ignore: bool +) -> None: + """Ignore an issue. + + Will raise if the issue does not exist. + """ + issue_registry = async_get_issue_registry(hass) + issue_registry.async_ignore(domain, issue_id, ignore) diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py new file mode 100644 index 00000000000..bd201f1007c --- /dev/null +++ b/homeassistant/components/repairs/issue_registry.py @@ -0,0 +1,205 @@ +"""Persistently store issues raised by integrations.""" +from __future__ import annotations + +import dataclasses +from datetime import datetime +from typing import Optional, cast + +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store +import homeassistant.util.dt as dt_util + +from .models import IssueSeverity + +DATA_REGISTRY = "issue_registry" +EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED = "repairs_issue_registry_updated" +STORAGE_KEY = "repairs.issue_registry" +STORAGE_VERSION = 1 +SAVE_DELAY = 10 + + +@dataclasses.dataclass(frozen=True) +class IssueEntry: + """Issue Registry Entry.""" + + active: bool + breaks_in_ha_version: str | None + created: datetime + dismissed_version: str | None + domain: str + is_fixable: bool | None + issue_id: str + # Used if an integration creates issues for other integrations (ie alerts) + issue_domain: str | None + learn_more_url: str | None + severity: IssueSeverity | None + translation_key: str | None + translation_placeholders: dict[str, str] | None + + +class IssueRegistry: + """Class to hold a registry of issues.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the issue registry.""" + self.hass = hass + self.issues: dict[tuple[str, str], IssueEntry] = {} + self._store = Store[dict[str, list[dict[str, Optional[str]]]]]( + hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) + + @callback + def async_get_issue(self, domain: str, issue_id: str) -> IssueEntry | None: + """Get issue by id.""" + return self.issues.get((domain, issue_id)) + + @callback + def async_get_or_create( + self, + domain: str, + issue_id: str, + *, + issue_domain: str | None = None, + breaks_in_ha_version: str | None = None, + is_fixable: bool, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, + ) -> IssueEntry: + """Get issue. Create if it doesn't exist.""" + + if (issue := self.async_get_issue(domain, issue_id)) is None: + issue = IssueEntry( + active=True, + breaks_in_ha_version=breaks_in_ha_version, + created=dt_util.utcnow(), + dismissed_version=None, + domain=domain, + is_fixable=is_fixable, + issue_domain=issue_domain, + issue_id=issue_id, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self.issues[(domain, issue_id)] = issue + self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "create", "domain": domain, "issue_id": issue_id}, + ) + else: + issue = self.issues[(domain, issue_id)] = dataclasses.replace( + issue, + active=True, + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, + issue_domain=issue_domain, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "update", "domain": domain, "issue_id": issue_id}, + ) + + return issue + + @callback + def async_delete(self, domain: str, issue_id: str) -> None: + """Delete issue.""" + if self.issues.pop((domain, issue_id), None) is None: + return + + self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "remove", "domain": domain, "issue_id": issue_id}, + ) + + @callback + def async_ignore(self, domain: str, issue_id: str, ignore: bool) -> IssueEntry: + """Ignore issue.""" + old = self.issues[(domain, issue_id)] + dismissed_version = ha_version if ignore else None + if old.dismissed_version == dismissed_version: + return old + + issue = self.issues[(domain, issue_id)] = dataclasses.replace( + old, + dismissed_version=dismissed_version, + ) + + self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "update", "domain": domain, "issue_id": issue_id}, + ) + + return issue + + async def async_load(self) -> None: + """Load the issue registry.""" + data = await self._store.async_load() + + issues: dict[tuple[str, str], IssueEntry] = {} + + if isinstance(data, dict): + for issue in data["issues"]: + assert issue["created"] and issue["domain"] and issue["issue_id"] + issues[(issue["domain"], issue["issue_id"])] = IssueEntry( + active=False, + breaks_in_ha_version=None, + created=cast(datetime, dt_util.parse_datetime(issue["created"])), + dismissed_version=issue["dismissed_version"], + domain=issue["domain"], + is_fixable=None, + issue_id=issue["issue_id"], + issue_domain=None, + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) + + self.issues = issues + + @callback + def async_schedule_save(self) -> None: + """Schedule saving the issue registry.""" + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: + """Return data of issue registry to store in a file.""" + data = {} + + data["issues"] = [ + { + "created": entry.created.isoformat(), + "dismissed_version": entry.dismissed_version, + "domain": entry.domain, + "issue_id": entry.issue_id, + } + for entry in self.issues.values() + ] + + return data + + +@callback +def async_get(hass: HomeAssistant) -> IssueRegistry: + """Get issue registry.""" + return cast(IssueRegistry, hass.data[DATA_REGISTRY]) + + +async def async_load(hass: HomeAssistant) -> None: + """Load issue registry.""" + assert DATA_REGISTRY not in hass.data + hass.data[DATA_REGISTRY] = IssueRegistry(hass) + await hass.data[DATA_REGISTRY].async_load() diff --git a/homeassistant/components/repairs/manifest.json b/homeassistant/components/repairs/manifest.json new file mode 100644 index 00000000000..f2013743a05 --- /dev/null +++ b/homeassistant/components/repairs/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "repairs", + "name": "Repairs", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/repairs", + "codeowners": ["@home-assistant/core"], + "dependencies": ["http"] +} diff --git a/homeassistant/components/repairs/models.py b/homeassistant/components/repairs/models.py new file mode 100644 index 00000000000..2a6eeb15269 --- /dev/null +++ b/homeassistant/components/repairs/models.py @@ -0,0 +1,29 @@ +"""Models for Repairs.""" +from __future__ import annotations + +from typing import Protocol + +from homeassistant import data_entry_flow +from homeassistant.backports.enum import StrEnum +from homeassistant.core import HomeAssistant + + +class IssueSeverity(StrEnum): + """Issue severity.""" + + CRITICAL = "critical" + ERROR = "error" + WARNING = "warning" + + +class RepairsFlow(data_entry_flow.FlowHandler): + """Handle a flow for fixing an issue.""" + + +class RepairsProtocol(Protocol): + """Define the format of repairs platforms.""" + + async def async_create_fix_flow( + self, hass: HomeAssistant, issue_id: str + ) -> RepairsFlow: + """Create a flow to fix a fixable issue.""" diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py new file mode 100644 index 00000000000..2e9fcc5f8e4 --- /dev/null +++ b/homeassistant/components/repairs/websocket_api.py @@ -0,0 +1,139 @@ +"""The repairs websocket API.""" +from __future__ import annotations + +import dataclasses +from http import HTTPStatus +from typing import Any + +from aiohttp import web +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.auth.permissions.const import POLICY_EDIT +from homeassistant.components import websocket_api +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import Unauthorized +from homeassistant.helpers.data_entry_flow import ( + FlowManagerIndexView, + FlowManagerResourceView, +) + +from .const import DOMAIN +from .issue_handler import async_ignore_issue +from .issue_registry import async_get as async_get_issue_registry + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the repairs websocket API.""" + websocket_api.async_register_command(hass, ws_ignore_issue) + websocket_api.async_register_command(hass, ws_list_issues) + + hass.http.register_view(RepairsFlowIndexView(hass.data[DOMAIN]["flow_manager"])) + hass.http.register_view(RepairsFlowResourceView(hass.data[DOMAIN]["flow_manager"])) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "repairs/ignore_issue", + vol.Required("domain"): str, + vol.Required("issue_id"): str, + vol.Required("ignore"): bool, + } +) +def ws_ignore_issue( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Fix an issue.""" + async_ignore_issue(hass, msg["domain"], msg["issue_id"], msg["ignore"]) + + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "repairs/list_issues", + } +) +@callback +def ws_list_issues( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Return a list of issues.""" + + def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: + result = {k: v for k, v in kv_pairs if k not in ("active")} + result["ignored"] = result["dismissed_version"] is not None + result["created"] = result["created"].isoformat() + return result + + issue_registry = async_get_issue_registry(hass) + issues = [ + dataclasses.asdict(issue, dict_factory=ws_dict) + for issue in issue_registry.issues.values() + if issue.active + ] + + connection.send_result(msg["id"], {"issues": issues}) + + +class RepairsFlowIndexView(FlowManagerIndexView): + """View to create issue fix flows.""" + + url = "/api/repairs/issues/fix" + name = "api:repairs:issues:fix" + + @RequestDataValidator( + vol.Schema( + { + vol.Required("handler"): str, + vol.Required("issue_id"): str, + }, + extra=vol.ALLOW_EXTRA, + ) + ) + async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: + """Handle a POST request.""" + if not request["hass_user"].is_admin: + raise Unauthorized(permission=POLICY_EDIT) + + try: + result = await self._flow_mgr.async_init( + data["handler"], + data={"issue_id": data["issue_id"]}, + ) + except data_entry_flow.UnknownHandler: + return self.json_message("Invalid handler specified", HTTPStatus.NOT_FOUND) + except data_entry_flow.UnknownStep: + return self.json_message( + "Handler does not support user", HTTPStatus.BAD_REQUEST + ) + + result = self._prepare_result_json(result) + + return self.json(result) + + +class RepairsFlowResourceView(FlowManagerResourceView): + """View to interact with the option flow manager.""" + + url = "/api/repairs/issues/fix/{flow_id}" + name = "api:repairs:issues:fix:resource" + + async def get(self, request: web.Request, flow_id: str) -> web.Response: + """Get the current state of a data_entry_flow.""" + if not request["hass_user"].is_admin: + raise Unauthorized(permission=POLICY_EDIT) + + return await super().get(request, flow_id) + + # pylint: disable=arguments-differ + async def post(self, request: web.Request, flow_id: str) -> web.Response: + """Handle a POST request.""" + if not request["hass_user"].is_admin: + raise Unauthorized(permission=POLICY_EDIT) + + # pylint: disable=no-value-for-parameter + return await super().post(request, flow_id) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index b517abafc86..3257a482c0c 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -440,6 +440,14 @@ def get_device_tuple_from_identifiers( return DeviceTuple(identifier2[1], identifier2[2], identifier2[3]) +def get_identifiers_from_device_tuple( + device_tuple: DeviceTuple, +) -> set[tuple[str, str]]: + """Calculate the device identifier from a device tuple.""" + # work around legacy identifier, being a multi tuple value + return {(DOMAIN, *device_tuple)} # type: ignore[arg-type] + + async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: @@ -456,6 +464,9 @@ class RfxtrxEntity(RestoreEntity): Contains the common logic for Rfxtrx lights and switches. """ + _attr_assumed_state = True + _attr_has_entity_name = True + _attr_should_poll = False _device: rfxtrxmod.RFXtrxDevice _event: rfxtrxmod.RFXtrxEvent | None @@ -466,11 +477,15 @@ class RfxtrxEntity(RestoreEntity): event: rfxtrxmod.RFXtrxEvent | None = None, ) -> None: """Initialize the device.""" - self._name = f"{device.type_string} {device.id_string}" + self._attr_device_info = DeviceInfo( + identifiers=get_identifiers_from_device_tuple(device_id), + model=device.type_string, + name=f"{device.type_string} {device.id_string}", + ) + self._attr_unique_id = "_".join(x for x in device_id) self._device = device self._event = event self._device_id = device_id - self._unique_id = "_".join(x for x in self._device_id) # If id_string is 213c7f2:1, the group_id is 213c7f2, and the device will respond to # group events regardless of their group indices. (self._group_id, _, _) = device.id_string.partition(":") @@ -484,16 +499,6 @@ class RfxtrxEntity(RestoreEntity): async_dispatcher_connect(self.hass, SIGNAL_EVENT, self._handle_event) ) - @property - def should_poll(self): - """No polling needed for a RFXtrx switch.""" - return False - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - @property def extra_state_attributes(self): """Return the device state attributes.""" @@ -501,25 +506,6 @@ class RfxtrxEntity(RestoreEntity): return None return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - @property - def assumed_state(self): - """Return true if unable to access real state of entity.""" - return True - - @property - def unique_id(self): - """Return unique identifier of remote device.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - return DeviceInfo( - identifiers={(DOMAIN, *self._device_id)}, - model=self._device.type_string, - name=f"{self._device.type_string} {self._device.id_string}", - ) - def _event_applies(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): """Check if event applies to me.""" if isinstance(event, rfxtrxmod.ControlEvent): @@ -559,7 +545,6 @@ class RfxtrxCommandEntity(RfxtrxEntity): ) -> None: """Initialzie a switch or light device.""" super().__init__(device, device_id, event=event) - self._state: bool | None = None async def _async_send(self, fun, *args): rfx_object = self.hass.data[DOMAIN][DATA_RFXOBJECT] diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index ef315c7b974..1a0fe698dcf 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -124,6 +124,9 @@ async def async_setup_entry( class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """A representation of a RFXtrx binary sensor.""" + _attr_force_update = True + """We should force updates. Repeated states have meaning.""" + def __init__( self, device: rfxtrxmod.RFXtrxDevice, @@ -140,7 +143,6 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self.entity_description = entity_description self._data_bits = data_bits self._off_delay = off_delay - self._state: bool | None = None self._delay_listener: CALLBACK_TYPE | None = None self._cmd_on = cmd_on self._cmd_off = cmd_off @@ -152,20 +154,10 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_ON + self._attr_is_on = old_state.state == STATE_ON - if self._state and self._off_delay is not None: - self._state = False - - @property - def force_update(self) -> bool: - """We should force updates. Repeated states have meaning.""" - return True - - @property - def is_on(self): - """Return true if the sensor state is True.""" - return self._state + if self.is_on and self._off_delay is not None: + self._attr_is_on = False def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): """Apply event for a lighting 4 device.""" @@ -174,22 +166,22 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): assert cmdstr cmd = int(cmdstr, 16) if cmd == self._cmd_on: - self._state = True + self._attr_is_on = True elif cmd == self._cmd_off: - self._state = False + self._attr_is_on = False else: - self._state = True + self._attr_is_on = True def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent): assert isinstance(event, (rfxtrxmod.SensorEvent, rfxtrxmod.ControlEvent)) if event.values.get("Command") in COMMAND_ON_LIST: - self._state = True + self._attr_is_on = True elif event.values.get("Command") in COMMAND_OFF_LIST: - self._state = False + self._attr_is_on = False elif event.values.get("Sensor Status") in SENSOR_STATUS_ON: - self._state = True + self._attr_is_on = True elif event.values.get("Sensor Status") in SENSOR_STATUS_OFF: - self._state = False + self._attr_is_on = False def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): """Apply command from rfxtrx.""" @@ -226,7 +218,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): def off_delay_listener(now): """Switch device off after a delay.""" self._delay_listener = None - self._state = False + self._attr_is_on = False self.async_write_ha_state() self._delay_listener = evt.async_call_later( diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 6bf49beb89a..5e1788194f5 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -71,6 +71,21 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): """Initialize the RFXtrx cover device.""" super().__init__(device, device_id, event) self._venetian_blind_mode = venetian_blind_mode + self._attr_is_closed: bool | None = True + + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP + ) + + if venetian_blind_mode in ( + CONST_VENETIAN_BLIND_MODE_US, + CONST_VENETIAN_BLIND_MODE_EU, + ): + self._attr_supported_features |= ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + ) async def async_added_to_hass(self) -> None: """Restore device state.""" @@ -79,31 +94,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_OPEN - - @property - def supported_features(self) -> int: - """Flag supported features.""" - supported_features = ( - CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP - ) - - if self._venetian_blind_mode in ( - CONST_VENETIAN_BLIND_MODE_US, - CONST_VENETIAN_BLIND_MODE_EU, - ): - supported_features |= ( - CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.STOP_TILT - ) - - return supported_features - - @property - def is_closed(self) -> bool: - """Return if the cover is closed.""" - return not self._state + self._attr_is_closed = old_state.state != STATE_OPEN async def async_open_cover(self, **kwargs: Any) -> None: """Move the cover up.""" @@ -113,7 +104,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): await self._async_send(self._device.send_up2sec) else: await self._async_send(self._device.send_open) - self._state = True + self._attr_is_closed = False self.async_write_ha_state() async def async_close_cover(self, **kwargs: Any) -> None: @@ -124,13 +115,13 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): await self._async_send(self._device.send_down2sec) else: await self._async_send(self._device.send_close) - self._state = False + self._attr_is_closed = True self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._async_send(self._device.send_stop) - self._state = True + self._attr_is_closed = False self.async_write_ha_state() async def async_open_cover_tilt(self, **kwargs: Any) -> None: @@ -150,7 +141,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" await self._async_send(self._device.send_stop) - self._state = True + self._attr_is_closed = False self.async_write_ha_state() def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): @@ -158,9 +149,9 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: - self._state = True + self._attr_is_closed = False elif event.values["Command"] in COMMAND_OFF_LIST: - self._state = False + self._attr_is_closed = True @callback def _handle_event(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 87f8c68aaac..198d0ffd3f1 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -56,7 +56,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - _brightness = 0 + _attr_brightness: int = 0 _device: rfxtrxmod.LightingDevice async def async_added_to_hass(self): @@ -66,37 +66,28 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_ON - self._brightness = old_state.attributes.get(ATTR_BRIGHTNESS) - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def is_on(self): - """Return true if device is on.""" - return self._state + self._attr_is_on = old_state.state == STATE_ON + if brightness := old_state.attributes.get(ATTR_BRIGHTNESS): + self._attr_brightness = int(brightness) async def async_turn_on(self, **kwargs): """Turn the device on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) - self._state = True + self._attr_is_on = True if brightness is None: await self._async_send(self._device.send_on) - self._brightness = 255 + self._attr_brightness = 255 else: await self._async_send(self._device.send_dim, brightness * 100 // 255) - self._brightness = brightness + self._attr_brightness = brightness self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" await self._async_send(self._device.send_off) - self._state = False - self._brightness = 0 + self._attr_is_on = False + self._attr_brightness = 0 self.async_write_ha_state() def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): @@ -104,12 +95,13 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: - self._state = True + self._attr_is_on = True elif event.values["Command"] in COMMAND_OFF_LIST: - self._state = False + self._attr_is_on = False elif event.values["Command"] == "Set level": - self._brightness = event.values["Dim level"] * 255 // 100 - self._state = self._brightness > 0 + brightness = event.values["Dim level"] * 255 // 100 + self._attr_brightness = brightness + self._attr_is_on = brightness > 0 @callback def _handle_event(self, event, device_id): diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 3111f0eabd7..86c3eabc922 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -63,12 +63,14 @@ class RfxtrxSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES = ( RfxtrxSensorEntityDescription( key="Barometer", + name="Barometer", device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_HPA, ), RfxtrxSensorEntityDescription( key="Battery numeric", + name="Battery", device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -77,42 +79,49 @@ SENSOR_TYPES = ( ), RfxtrxSensorEntityDescription( key="Current", + name="Current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 1", + name="Current Ch. 1", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 2", + name="Current Ch. 2", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 3", + name="Current Ch. 3", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Energy usage", + name="Instantaneous power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), RfxtrxSensorEntityDescription( key="Humidity", + name="Humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), RfxtrxSensorEntityDescription( key="Rssi numeric", + name="Signal strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -121,86 +130,104 @@ SENSOR_TYPES = ( ), RfxtrxSensorEntityDescription( key="Temperature", + name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Temperature2", + name="Temperature 2", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Total usage", + name="Total energy usage", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), RfxtrxSensorEntityDescription( key="Voltage", + name="Voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, ), RfxtrxSensorEntityDescription( key="Wind direction", + name="Wind direction", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DEGREE, ), RfxtrxSensorEntityDescription( key="Rain rate", + name="Rain rate", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), RfxtrxSensorEntityDescription( key="Sound", + name="Sound", ), RfxtrxSensorEntityDescription( key="Sensor Status", + name="Sensor status", ), RfxtrxSensorEntityDescription( key="Count", + name="Count", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Counter value", + name="Counter value", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Chill", + name="Chill", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Wind average speed", + name="Wind average speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Wind gust", + name="Wind gust", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Rain total", + name="Rain total", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=LENGTH_MILLIMETERS, ), RfxtrxSensorEntityDescription( key="Forecast", + name="Forecast status", ), RfxtrxSensorEntityDescription( key="Forecast numeric", + name="Forecast", ), RfxtrxSensorEntityDescription( key="Humidity status", + name="Humidity status", ), RfxtrxSensorEntityDescription( key="UV", + name="UV index", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UV_INDEX, ), @@ -246,16 +273,16 @@ async def async_setup_entry( class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Representation of a RFXtrx sensor.""" + _attr_force_update = True + """We should force updates. Repeated states have meaning.""" + entity_description: RfxtrxSensorEntityDescription def __init__(self, device, device_id, entity_description, event=None): """Initialize the sensor.""" super().__init__(device, device_id, event=event) self.entity_description = entity_description - self._name = f"{device.type_string} {device.id_string} {entity_description.key}" - self._unique_id = "_".join( - x for x in (*self._device_id, entity_description.key) - ) + self._attr_unique_id = "_".join(x for x in (*device_id, entity_description.key)) async def async_added_to_hass(self): """Restore device state.""" @@ -276,16 +303,6 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): value = self._event.values.get(self.entity_description.key) return self.entity_description.convert(value) - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def force_update(self) -> bool: - """We should force updates. Repeated states have meaning.""" - return True - @callback def _handle_event(self, event, device_id): """Check if event applies to me and update.""" diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index fc826b4ebe6..f164e54b212 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -94,7 +94,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_ON + self._attr_is_on = old_state.state == STATE_ON def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): """Apply event for a lighting 4 device.""" @@ -103,18 +103,18 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): assert cmdstr cmd = int(cmdstr, 16) if cmd == self._cmd_on: - self._state = True + self._attr_is_on = True elif cmd == self._cmd_off: - self._state = False + self._attr_is_on = False else: - self._state = True + self._attr_is_on = True def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent) -> None: assert isinstance(event, rfxtrxmod.ControlEvent) if event.values["Command"] in COMMAND_ON_LIST: - self._state = True + self._attr_is_on = True elif event.values["Command"] in COMMAND_OFF_LIST: - self._state = False + self._attr_is_on = False def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" @@ -134,18 +134,13 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self.async_write_ha_state() - @property - def is_on(self): - """Return true if device is on.""" - return self._state - async def async_turn_on(self, **kwargs): """Turn the device on.""" if self._cmd_on is not None: await self._async_send(self._device.send_command, self._cmd_on) else: await self._async_send(self._device.send_on) - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): @@ -154,5 +149,5 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): await self._async_send(self._device.send_command, self._cmd_off) else: await self._async_send(self._device.send_off) - self._state = False + self._attr_is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 158411ef618..ebdde5c9428 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -45,7 +45,7 @@ "send_status": "\u00c1llapotfriss\u00edt\u00e9s k\u00fcld\u00e9se: {subtype}" }, "trigger_type": { - "command": "Be\u00e9rkezett parancs: {alt\u00edpus}", + "command": "Be\u00e9rkezett parancs: {subtype}", "status": "Be\u00e9rkezett st\u00e1tusz: {subtype}" } }, diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json index 9b22d34af58..755621839c8 100644 --- a/homeassistant/components/rfxtrx/translations/ja.json +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/rhasspy/__init__.py b/homeassistant/components/rhasspy/__init__.py new file mode 100644 index 00000000000..669d81952d4 --- /dev/null +++ b/homeassistant/components/rhasspy/__init__.py @@ -0,0 +1,15 @@ +"""The Rhasspy integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Rhasspy from a config entry.""" + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return True diff --git a/homeassistant/components/rhasspy/config_flow.py b/homeassistant/components/rhasspy/config_flow.py new file mode 100644 index 00000000000..69ed802f817 --- /dev/null +++ b/homeassistant/components/rhasspy/config_flow.py @@ -0,0 +1,29 @@ +"""Config flow for Rhasspy integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Rhasspy.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is None: + return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + + return self.async_create_entry(title="Rhasspy", data={}) diff --git a/homeassistant/components/rhasspy/const.py b/homeassistant/components/rhasspy/const.py new file mode 100644 index 00000000000..127f20032ac --- /dev/null +++ b/homeassistant/components/rhasspy/const.py @@ -0,0 +1,3 @@ +"""Constants for the Rhasspy integration.""" + +DOMAIN = "rhasspy" diff --git a/homeassistant/components/rhasspy/manifest.json b/homeassistant/components/rhasspy/manifest.json new file mode 100644 index 00000000000..8b11e231b8c --- /dev/null +++ b/homeassistant/components/rhasspy/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "rhasspy", + "name": "Rhasspy", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/rhasspy", + "dependencies": ["intent"], + "codeowners": ["@balloob", "@synesthesiam"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/rhasspy/strings.json b/homeassistant/components/rhasspy/strings.json new file mode 100644 index 00000000000..4d2111ebd8a --- /dev/null +++ b/homeassistant/components/rhasspy/strings.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "description": "Do you want to enable Rhasspy support?" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/rhasspy/translations/ca.json b/homeassistant/components/rhasspy/translations/ca.json new file mode 100644 index 00000000000..b016b21a2e2 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/ca.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols activar el suport de Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/de.json b/homeassistant/components/rhasspy/translations/de.json new file mode 100644 index 00000000000..953cb89400a --- /dev/null +++ b/homeassistant/components/rhasspy/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest du die Rhasspy-Unterst\u00fctzung aktivieren?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/el.json b/homeassistant/components/rhasspy/translations/el.json new file mode 100644 index 00000000000..fe181ecdb04 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/el.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 Rhasspy;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/en.json b/homeassistant/components/rhasspy/translations/en.json new file mode 100644 index 00000000000..af826fbf27d --- /dev/null +++ b/homeassistant/components/rhasspy/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to enable Rhasspy support?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/et.json b/homeassistant/components/rhasspy/translations/et.json new file mode 100644 index 00000000000..8182211cdef --- /dev/null +++ b/homeassistant/components/rhasspy/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine." + }, + "step": { + "user": { + "description": "Kas soovid Rhaspy toe lubada?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/fr.json b/homeassistant/components/rhasspy/translations/fr.json new file mode 100644 index 00000000000..6fce2e23902 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous activer la prise en charge de Rhasspy\u00a0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/hu.json b/homeassistant/components/rhasspy/translations/hu.json new file mode 100644 index 00000000000..8297d6af059 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva. Csak egyetlen konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "Szeretn\u00e9 enged\u00e9lyezni a Rhasspy t\u00e1mogat\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/id.json b/homeassistant/components/rhasspy/translations/id.json new file mode 100644 index 00000000000..7a0b5445bcd --- /dev/null +++ b/homeassistant/components/rhasspy/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin mengaktifkan dukungan Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/it.json b/homeassistant/components/rhasspy/translations/it.json new file mode 100644 index 00000000000..55ce19aff24 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/it.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "user": { + "description": "Vuoi abilitare il supporto Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/ja.json b/homeassistant/components/rhasspy/translations/ja.json new file mode 100644 index 00000000000..d6f1b06b228 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + }, + "step": { + "user": { + "description": "Rhasspy\u306e\u30b5\u30dd\u30fc\u30c8\u3092\u6709\u52b9\u306b\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/nl.json b/homeassistant/components/rhasspy/translations/nl.json new file mode 100644 index 00000000000..7089a156d1b --- /dev/null +++ b/homeassistant/components/rhasspy/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wilt u Rhasspy-ondersteuning inschakelen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/no.json b/homeassistant/components/rhasspy/translations/no.json new file mode 100644 index 00000000000..4dc3393a982 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/no.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Kun \u00e9n konfigurert instans i gangen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/pl.json b/homeassistant/components/rhasspy/translations/pl.json new file mode 100644 index 00000000000..79db07a077f --- /dev/null +++ b/homeassistant/components/rhasspy/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Czy chcesz w\u0142\u0105czy\u0107 obs\u0142ug\u0119 Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/pt-BR.json b/homeassistant/components/rhasspy/translations/pt-BR.json new file mode 100644 index 00000000000..0e62dceb57d --- /dev/null +++ b/homeassistant/components/rhasspy/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 foi configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja habilitar o suporte ao Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/pt.json b/homeassistant/components/rhasspy/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/ru.json b/homeassistant/components/rhasspy/translations/ru.json new file mode 100644 index 00000000000..ce319f9537e --- /dev/null +++ b/homeassistant/components/rhasspy/translations/ru.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/tr.json b/homeassistant/components/rhasspy/translations/tr.json new file mode 100644 index 00000000000..a8f9bf1204c --- /dev/null +++ b/homeassistant/components/rhasspy/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Rhasspy deste\u011fini etkinle\u015ftirmek istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/zh-Hant.json b/homeassistant/components/rhasspy/translations/zh-Hant.json new file mode 100644 index 00000000000..ce30b5189a6 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u555f Rhasspy \u652f\u63f4\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 5f3933a09c5..5a9c19ed36f 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -21,19 +22,21 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import ( - DATA_ACCOUNT, - DATA_COORDINATOR, - DOMAIN, - LOGGER, - SENSOR_TYPE_NEXT_PICKUP, -) +from .const import DOMAIN, LOGGER, SENSOR_TYPE_NEXT_PICKUP DEFAULT_UPDATE_INTERVAL = timedelta(hours=1) PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH] +@dataclass +class RidwellData: + """Define an object to be stored in `hass.data`.""" + + accounts: dict[str, RidwellAccount] + coordinator: DataUpdateCoordinator + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ridwell from a config entry.""" session = aiohttp_client.async_get_clientsession(hass) @@ -77,12 +80,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_ACCOUNT: accounts, - DATA_COORDINATOR: coordinator, - } + hass.data[DOMAIN][entry.entry_id] = RidwellData( + accounts=accounts, coordinator=coordinator + ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -124,6 +126,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class RidwellEntity(CoordinatorEntity): """Define a base Ridwell entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, diff --git a/homeassistant/components/ridwell/const.py b/homeassistant/components/ridwell/const.py index bc1e78e2d93..69c5ead5277 100644 --- a/homeassistant/components/ridwell/const.py +++ b/homeassistant/components/ridwell/const.py @@ -5,7 +5,4 @@ DOMAIN = "ridwell" LOGGER = logging.getLogger(__package__) -DATA_ACCOUNT = "account" -DATA_COORDINATOR = "coordinator" - SENSOR_TYPE_NEXT_PICKUP = "next_pickup" diff --git a/homeassistant/components/ridwell/diagnostics.py b/homeassistant/components/ridwell/diagnostics.py index fce89a639ad..3f29165842f 100644 --- a/homeassistant/components/ridwell/diagnostics.py +++ b/homeassistant/components/ridwell/diagnostics.py @@ -6,19 +6,17 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DATA_COORDINATOR, DOMAIN +from . import RidwellData +from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] + data: RidwellData = hass.data[DOMAIN][entry.entry_id] return { - "data": [dataclasses.asdict(event) for event in coordinator.data.values()], + "data": [dataclasses.asdict(event) for event in data.coordinator.data.values()] } diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 44bab16691a..9b7a4ab6954 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -18,8 +18,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import RidwellEntity -from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN, SENSOR_TYPE_NEXT_PICKUP +from . import RidwellData, RidwellEntity +from .const import DOMAIN, SENSOR_TYPE_NEXT_PICKUP ATTR_CATEGORY = "category" ATTR_PICKUP_STATE = "pickup_state" @@ -28,7 +28,7 @@ ATTR_QUANTITY = "quantity" SENSOR_DESCRIPTION = SensorEntityDescription( key=SENSOR_TYPE_NEXT_PICKUP, - name="Ridwell Pickup", + name="Ridwell pickup", device_class=SensorDeviceClass.DATE, ) @@ -37,13 +37,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - accounts = hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RidwellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - RidwellSensor(coordinator, account, SENSOR_DESCRIPTION) - for account in accounts.values() + RidwellSensor(data.coordinator, account, SENSOR_DESCRIPTION) + for account in data.accounts.values() ] ) diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py index 7bdea622507..d8e228be7db 100644 --- a/homeassistant/components/ridwell/switch.py +++ b/homeassistant/components/ridwell/switch.py @@ -12,8 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RidwellEntity -from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN +from . import RidwellData, RidwellEntity +from .const import DOMAIN SWITCH_TYPE_OPT_IN = "opt_in" @@ -28,13 +28,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - accounts = hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RidwellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - RidwellSwitch(coordinator, account, SWITCH_DESCRIPTION) - for account in accounts.values() + RidwellSwitch(data.coordinator, account, SWITCH_DESCRIPTION) + for account in data.accounts.values() ] ) diff --git a/homeassistant/components/ridwell/translations/cs.json b/homeassistant/components/ridwell/translations/cs.json index 72df4a96818..5d43feee500 100644 --- a/homeassistant/components/ridwell/translations/cs.json +++ b/homeassistant/components/ridwell/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/ridwell/translations/ja.json b/homeassistant/components/ridwell/translations/ja.json index 33d7e12ebd4..746d237457f 100644 --- a/homeassistant/components/ridwell/translations/ja.json +++ b/homeassistant/components/ridwell/translations/ja.json @@ -14,7 +14,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/ridwell/translations/pt.json b/homeassistant/components/ridwell/translations/pt.json new file mode 100644 index 00000000000..3d5061419fc --- /dev/null +++ b/homeassistant/components/ridwell/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 0d8f87eef3c..fb037eca05d 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.services.has_service(DOMAIN, "update"): return True diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 362fd615700..fea3ee63aac 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -1,9 +1,8 @@ """The Risco integration.""" -import asyncio from datetime import timedelta import logging -from pyrisco import CannotConnectError, OperationError, RiscoAPI, UnauthorizedError +from pyrisco import CannotConnectError, OperationError, RiscoCloud, UnauthorizedError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -31,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Risco from a config entry.""" data = entry.data - risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) + risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) try: await risco.login(async_get_clientsession(hass)) except CannotConnectError as error: @@ -56,16 +55,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: EVENTS_COORDINATOR: events_coordinator, } - async def start_platforms(): - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - await events_coordinator.async_refresh() - - hass.async_create_task(start_platforms()) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await events_coordinator.async_refresh() return True diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index e2e139a19e0..5f8f40cb5f7 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from pyrisco import CannotConnectError, RiscoAPI, UnauthorizedError +from pyrisco import CannotConnectError, RiscoCloud, UnauthorizedError import voluptuous as vol from homeassistant import config_entries, core @@ -52,7 +52,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) + risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) try: await risco.login(async_get_clientsession(hass)) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 736adcf0c35..a7c07af3e18 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.3.1"], + "requirements": ["pyrisco==0.5.0"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index c45a762ca9a..441eb04cbe8 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index f24d08909b8..7f922c2eea5 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/roku/binary_sensor.py b/homeassistant/components/roku/binary_sensor.py index 5b6da073dd1..243ea994dfa 100644 --- a/homeassistant/components/roku/binary_sensor.py +++ b/homeassistant/components/roku/binary_sensor.py @@ -36,7 +36,7 @@ class RokuBinarySensorEntityDescription( BINARY_SENSORS: tuple[RokuBinarySensorEntityDescription, ...] = ( RokuBinarySensorEntityDescription( key="headphones_connected", - name="Headphones Connected", + name="Headphones connected", icon="mdi:headphones", value_fn=lambda device: device.info.headphones_connected, ), @@ -49,14 +49,14 @@ BINARY_SENSORS: tuple[RokuBinarySensorEntityDescription, ...] = ( ), RokuBinarySensorEntityDescription( key="supports_ethernet", - name="Supports Ethernet", + name="Supports ethernet", icon="mdi:ethernet", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.ethernet_support, ), RokuBinarySensorEntityDescription( key="supports_find_remote", - name="Supports Find Remote", + name="Supports find remote", icon="mdi:remote", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.supports_find_remote, diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index 39373c96c6a..a85024f8220 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -25,30 +25,32 @@ class RokuEntity(CoordinatorEntity[RokuDataUpdateCoordinator]): if description is not None: self.entity_description = description - self._attr_name = f"{coordinator.data.info.name} {description.name}" - if device_id is not None: + + if device_id is None: + self._attr_name = f"{coordinator.data.info.name} {description.name}" + + if device_id is not None: + self._attr_has_entity_name = True + + if description is not None: self._attr_unique_id = f"{device_id}_{description.key}" + else: + self._attr_unique_id = device_id - @property - def device_info(self) -> DeviceInfo | None: - """Return device information about this Roku device.""" - if self._device_id is None: - return None - - return DeviceInfo( - identifiers={(DOMAIN, self._device_id)}, - connections={ - (CONNECTION_NETWORK_MAC, mac_address) - for mac_address in ( - self.coordinator.data.info.wifi_mac, - self.coordinator.data.info.ethernet_mac, - ) - if mac_address is not None - }, - name=self.coordinator.data.info.name, - manufacturer=self.coordinator.data.info.brand, - model=self.coordinator.data.info.model_name, - hw_version=self.coordinator.data.info.model_number, - sw_version=self.coordinator.data.info.version, - suggested_area=self.coordinator.data.info.device_location, - ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + connections={ + (CONNECTION_NETWORK_MAC, mac_address) + for mac_address in ( + self.coordinator.data.info.wifi_mac, + self.coordinator.data.info.ethernet_mac, + ) + if mac_address is not None + }, + name=self.coordinator.data.info.name, + manufacturer=self.coordinator.data.info.brand, + model=self.coordinator.data.info.model_name, + hw_version=self.coordinator.data.info.model_number, + sw_version=self.coordinator.data.info.version, + suggested_area=self.coordinator.data.info.device_location, + ) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index a47432694dd..d7b9a3489c9 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -99,7 +99,15 @@ async def async_setup_entry( """Set up the Roku config entry.""" coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] unique_id = coordinator.data.info.serial_number - async_add_entities([RokuMediaPlayer(unique_id, coordinator)], True) + async_add_entities( + [ + RokuMediaPlayer( + device_id=unique_id, + coordinator=coordinator, + ) + ], + True, + ) platform = entity_platform.async_get_current_platform() @@ -127,18 +135,6 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) - def __init__( - self, unique_id: str | None, coordinator: RokuDataUpdateCoordinator - ) -> None: - """Initialize the Roku device.""" - super().__init__( - coordinator=coordinator, - device_id=unique_id, - ) - - self._attr_name = coordinator.data.info.name - self._attr_unique_id = unique_id - def _media_playback_trackable(self) -> bool: """Detect if we have enough media data to track playback.""" if self.coordinator.data.media is None or self.coordinator.data.media.live: diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 6d1312c0b03..fceac67a477 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -21,24 +21,22 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Load Roku remote based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] unique_id = coordinator.data.info.serial_number - async_add_entities([RokuRemote(unique_id, coordinator)], True) + async_add_entities( + [ + RokuRemote( + device_id=unique_id, + coordinator=coordinator, + ) + ], + True, + ) class RokuRemote(RokuEntity, RemoteEntity): """Device that sends commands to an Roku.""" - def __init__(self, unique_id: str, coordinator: RokuDataUpdateCoordinator) -> None: - """Initialize the Roku device.""" - super().__init__( - device_id=unique_id, - coordinator=coordinator, - ) - - self._attr_name = coordinator.data.info.name - self._attr_unique_id = unique_id - @property def is_on(self) -> bool: """Return true if device is on.""" diff --git a/homeassistant/components/roku/translations/bg.json b/homeassistant/components/roku/translations/bg.json index ef4bfded40c..efb4c18e1b0 100644 --- a/homeassistant/components/roku/translations/bg.json +++ b/homeassistant/components/roku/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "discovery_confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 31b5187a195..641c814d122 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b CANCEL_STOP: cancel_stop, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if not config_entry.update_listeners: config_entry.add_update_listener(async_update_options) diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index e109b6e7043..b4126e202d1 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -19,7 +19,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rni az eszk\u00f6zr\u0151l. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", + "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rni az eszk\u00f6zr\u0151l. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", "title": "Jelsz\u00f3 megad\u00e1sa" }, "manual": { diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json index 84fe0402f15..5e40221cec6 100644 --- a/homeassistant/components/roomba/translations/pt.json +++ b/homeassistant/components/roomba/translations/pt.json @@ -4,13 +4,23 @@ "not_irobot_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo iRobot" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente" + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "flow_title": "iRobot {name} ({host})", "step": { "link": { "title": "Recuperar Palavra-passe" }, + "link_manual": { + "data": { + "password": "Palavra-passe" + } + }, + "manual": { + "data": { + "host": "Anfitri\u00e3o" + } + }, "user": { "data": { "host": "Servidor" diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index 9e5c38f0211..9969b694895 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -1,12 +1,14 @@ """Roon (www.roonlabs.com) component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .const import CONF_ROON_NAME, DOMAIN from .server import RoonServer +PLATFORMS = [Platform.MEDIA_PLAYER] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a roonserver from a config entry.""" @@ -28,10 +30,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manufacturer="Roonlabs", name=f"Roon Core ({name})", ) + + # initialize media_player platform + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + return False + roonserver = hass.data[DOMAIN].pop(entry.entry_id) return await roonserver.async_reset() diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index d5e4cded08d..997db44583d 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -4,7 +4,7 @@ import logging from roonapi import RoonApi, RoonDiscovery -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, Platform +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.dt import utcnow @@ -13,7 +13,6 @@ from .const import CONF_ROON_ID, ROON_APPINFO _LOGGER = logging.getLogger(__name__) INITIAL_SYNC_INTERVAL = 5 FULL_SYNC_INTERVAL = 30 -PLATFORMS = [Platform.MEDIA_PLAYER] class RoonServer: @@ -53,7 +52,6 @@ class RoonServer: (host, port) = get_roon_host() return RoonApi(ROON_APPINFO, token, host, port, blocking_init=True) - hass = self.hass core_id = self.config_entry.data.get(CONF_ROON_ID) self.roonapi = await self.hass.async_add_executor_job(get_roon_api) @@ -67,9 +65,6 @@ class RoonServer: core_id if core_id is not None else self.config_entry.data[CONF_HOST] ) - # initialize media_player platform - hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) - # Initialize Roon background polling asyncio.create_task(self.async_do_loop()) diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index aa536aeeddd..cf5d1609dd3 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -13,7 +13,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "Nem siker\u00fclt felfedezni a Roon-kiszolg\u00e1l\u00f3t. K\u00e9rj\u00fck, adja meg g\u00e9p c\u00edm\u00e9t \u00e9s portj\u00e1t." + "description": "Nem siker\u00fclt felfedezni a Roon-kiszolg\u00e1l\u00f3t. K\u00e9rem, adja meg g\u00e9p c\u00edm\u00e9t \u00e9s portj\u00e1t." }, "link": { "description": "Enged\u00e9lyeznie kell az Home Assistantot a Roonban. Miut\u00e1n r\u00e1kattintott a Mehet gombra, nyissa meg a Roon Core alkalmaz\u00e1st, nyissa meg a Be\u00e1ll\u00edt\u00e1sokat, \u00e9s enged\u00e9lyezze a Home Assistant funkci\u00f3t a B\u0151v\u00edtm\u00e9nyek lapon.", diff --git a/homeassistant/components/roon/translations/pt.json b/homeassistant/components/roon/translations/pt.json index 1e12fdcfcba..105f0b64676 100644 --- a/homeassistant/components/roon/translations/pt.json +++ b/homeassistant/components/roon/translations/pt.json @@ -6,6 +6,13 @@ "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "fallback": { + "data": { + "host": "Servidor" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 8647ab38a78..79504f17d65 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -8,7 +8,7 @@ PLATFORMS = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Raspberry Pi Power Supply Checker from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/rpi_power/translations/ja.json b/homeassistant/components/rpi_power/translations/ja.json index 26aee66af5b..2c2dff7e51e 100644 --- a/homeassistant/components/rpi_power/translations/ja.json +++ b/homeassistant/components/rpi_power/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u3053\u306e\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306b\u5fc5\u8981\u306a\u30b7\u30b9\u30c6\u30e0\u30af\u30e9\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u30ab\u30fc\u30cd\u30eb\u304c\u6700\u65b0\u3067\u3001\u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/ja.json b/homeassistant/components/rtsp_to_webrtc/translations/ja.json index 6359d66f50a..2f6c9eb0f80 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/ja.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "server_failure": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u304c\u30a8\u30e9\u30fc\u3092\u8fd4\u3057\u307e\u3057\u305f\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "server_unreachable": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u3068\u306e\u901a\u4fe1\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_url": "\u6709\u52b9\u306aRTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4f8b: https://example.com", @@ -19,7 +19,7 @@ "data": { "server_url": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL \u4f8b: https://example.com" }, - "description": "RTSPtoWebRTC\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092WebRTC\u306b\u5909\u63db\u3059\u308b\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "description": "RTSPtoWebRTC\u7d71\u5408\u306f\u3001RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092WebRTC\u306b\u5909\u63db\u3059\u308b\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "RTSPtoWebRTC\u306e\u8a2d\u5b9a" } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/pt.json b/homeassistant/components/rtsp_to_webrtc/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/rtsp_to_webrtc/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 62ed4949e05..5861486457f 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENERS: [], } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 4b9b7a2c8d0..e844f478322 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -2,7 +2,7 @@ "domain": "russound_rio", "name": "Russound RIO", "documentation": "https://www.home-assistant.io/integrations/russound_rio", - "requirements": ["russound_rio==0.1.7"], + "requirements": ["russound_rio==0.1.8"], "codeowners": [], "iot_class": "local_push", "loggers": ["russound_rio"] diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index c03d27fefe5..aca8d1cd9f4 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -242,7 +242,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sabnzbd/translations/pt.json b/homeassistant/components/sabnzbd/translations/pt.json new file mode 100644 index 00000000000..20bba0ede4b --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index b870aab62d4..0a7fd4b3378 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1,7 +1,7 @@ """The Samsung TV integration.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Coroutine, Mapping from functools import partial import socket from typing import Any @@ -131,7 +131,7 @@ class DebouncedEntryReloader: self.hass = hass self.entry = entry self.token = self.entry.data.get(CONF_TOKEN) - self._debounced_reload = Debouncer( + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 9cd068ef409..8523231e084 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -6,7 +6,7 @@ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", - "wakeonlan==2.0.1", + "wakeonlan==2.1.0", "async-upnp-client==0.31.2" ], "ssdp": [ diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index b53f50ff74a..d4f37d72922 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -12,7 +12,7 @@ }, "error": { "auth_missing": "Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV be\u00e1ll\u00edt\u00e1sait Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", - "invalid_pin": "A PIN k\u00f3d \u00e9rv\u00e9nytelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra." + "invalid_pin": "A PIN k\u00f3d \u00e9rv\u00e9nytelen, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra." }, "flow_title": "{device}", "step": { @@ -20,7 +20,7 @@ "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {device} k\u00e9sz\u00fcl\u00e9k\u00e9t? Ha kor\u00e1bban m\u00e9g sosem csatlakoztatta Home Assistanthoz, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r." }, "encrypted_pairing": { - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" + "description": "K\u00e9rem, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" }, "pairing": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {device} k\u00e9sz\u00fcl\u00e9k\u00e9t? Ha kor\u00e1bban m\u00e9g sosem csatlakoztatta Home Assistanthoz, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r." @@ -29,7 +29,7 @@ "description": "A bek\u00fcld\u00e9s ut\u00e1n, fogadja el a {device} felugr\u00f3 ablak\u00e1ban l\u00e1that\u00f3 \u00fczenetet, mely 30 m\u00e1sodpercig \u00e1ll rendelkez\u00e9sre, vagy adja meg a PIN k\u00f3dot." }, "reauth_confirm_encrypted": { - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" + "description": "K\u00e9rem, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index b2cd242c7da..c0c3fb735c1 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -5,7 +5,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, - "flow_title": "TV Samsung: {model}", + "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index b1ccbb354a9..4f8ea3d1481 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -2,7 +2,7 @@ "domain": "scrape", "name": "Scrape", "documentation": "https://www.home-assistant.io/integrations/scrape", - "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], + "requirements": ["beautifulsoup4==4.11.1", "lxml==4.9.1"], "after_dependencies": ["rest"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling" diff --git a/homeassistant/components/scrape/translations/pt.json b/homeassistant/components/scrape/translations/pt.json new file mode 100644 index 00000000000..003da0eed66 --- /dev/null +++ b/homeassistant/components/scrape/translations/pt.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "name": "Nome", + "unit_of_measurement": "Unidade de Medida" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unidade de Medida" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/ru.json b/homeassistant/components/scrape/translations/ru.json new file mode 100644 index 00000000000..2d014592e85 --- /dev/null +++ b/homeassistant/components/scrape/translations/ru.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", + "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "device_class": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", + "index": "\u0418\u043d\u0434\u0435\u043a\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", + "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c", + "state_class": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "attribute": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0435\u0433\u0430.", + "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", + "device_class": "\u0422\u0438\u043f/\u043a\u043b\u0430\u0441\u0441 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435.", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", + "index": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0438\u0437 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u043c CSS \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.", + "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "select": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0442\u0435\u0433 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043a\u0430\u0442\u044c. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u0432 CSS Beautifulsoup.", + "state_class": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 state_class \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", + "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430.", + "verify_ssl": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442/\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 SSL/TLS. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441\u0430\u043c\u043e\u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", + "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "device_class": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", + "index": "\u0418\u043d\u0434\u0435\u043a\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", + "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c", + "state_class": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "attribute": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0435\u0433\u0430.", + "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", + "device_class": "\u0422\u0438\u043f/\u043a\u043b\u0430\u0441\u0441 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435.", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", + "index": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0438\u0437 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u043c CSS \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.", + "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "select": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0442\u0435\u0433 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043a\u0430\u0442\u044c. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u0432 CSS Beautifulsoup.", + "state_class": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 state_class \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", + "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430.", + "verify_ssl": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442/\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 SSL/TLS. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441\u0430\u043c\u043e\u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index fe7d8a79afd..41e0638c634 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/screenlogic/translations/pt.json b/homeassistant/components/screenlogic/translations/pt.json new file mode 100644 index 00000000000..218e55c6d5f --- /dev/null +++ b/homeassistant/components/screenlogic/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "gateway_entry": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/__init__.py b/homeassistant/components/season/__init__.py index 6d4a2974522..f67abee3bea 100644 --- a/homeassistant/components/season/__init__.py +++ b/homeassistant/components/season/__init__.py @@ -7,7 +7,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 216475f0cdf..5fc161062bb 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -14,6 +14,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow @@ -121,13 +123,18 @@ class SeasonSensorEntity(SensorEntity): """Representation of the current season.""" _attr_device_class = "season__season" + _attr_has_entity_name = True def __init__(self, entry: ConfigEntry, hemisphere: str) -> None: """Initialize the season.""" - self._attr_name = entry.title self._attr_unique_id = entry.entry_id self.hemisphere = hemisphere self.type = entry.data[CONF_TYPE] + self._attr_device_info = DeviceInfo( + name=entry.title, + identifiers={(DOMAIN, entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) def update(self) -> None: """Update season.""" diff --git a/homeassistant/components/season/translations/pt.json b/homeassistant/components/season/translations/pt.json index b7bb07e9522..c8d0ddf9f70 100644 --- a/homeassistant/components/season/translations/pt.json +++ b/homeassistant/components/season/translations/pt.json @@ -1,7 +1,14 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "type": "Defini\u00e7\u00e3o do tipo de esta\u00e7\u00e3o" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.pt.json b/homeassistant/components/season/translations/sensor.pt.json index 4c81e432350..3f157f43c79 100644 --- a/homeassistant/components/season/translations/sensor.pt.json +++ b/homeassistant/components/season/translations/sensor.pt.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "Outono", + "spring": "Primavera", + "summer": "Ver\u00e3o", + "winter": "Inverno" + }, "season__season__": { "autumn": "Outono", "spring": "Primavera", diff --git a/homeassistant/components/select/translations/cs.json b/homeassistant/components/select/translations/cs.json new file mode 100644 index 00000000000..32d6cadee9e --- /dev/null +++ b/homeassistant/components/select/translations/cs.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Zm\u011bnit mo\u017enost {entity_name}" + } + }, + "title": "V\u00fdb\u011br" +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/pt.json b/homeassistant/components/select/translations/pt.json new file mode 100644 index 00000000000..aa92ddaf4c3 --- /dev/null +++ b/homeassistant/components/select/translations/pt.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Alterar op\u00e7\u00e3o {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index e938f7132e0..82b25802b02 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -130,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def async_sense_update(_): """Retrieve latest state.""" diff --git a/homeassistant/components/sense/translations/bg.json b/homeassistant/components/sense/translations/bg.json index 2be0802eef9..f81ad124c51 100644 --- a/homeassistant/components/sense/translations/bg.json +++ b/homeassistant/components/sense/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json index 437ce96d9f1..50dbc070ac9 100644 --- a/homeassistant/components/sense/translations/ja.json +++ b/homeassistant/components/sense/translations/ja.json @@ -14,8 +14,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "Sense\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {email} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Sense\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {email} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/sense/translations/pt.json b/homeassistant/components/sense/translations/pt.json index e3b78cd8e42..1b8b4e2dc18 100644 --- a/homeassistant/components/sense/translations/pt.json +++ b/homeassistant/components/sense/translations/pt.json @@ -9,6 +9,12 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_validate": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/senseme/__init__.py b/homeassistant/components/senseme/__init__.py index 7a64a23002f..a744dd0d0b8 100644 --- a/homeassistant/components/senseme/__init__.py +++ b/homeassistant/components/senseme/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await device.async_update(not status) hass.data[DOMAIN][entry.entry_id] = device - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/senseme/translations/pt.json b/homeassistant/components/senseme/translations/pt.json new file mode 100644 index 00000000000..4c3266a6022 --- /dev/null +++ b/homeassistant/components/senseme/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + }, + "step": { + "manual": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index dc02e9ee686..ee3cba7d001 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index ed280aab4fe..3a61570701d 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -55,7 +55,7 @@ class SensiboDeviceBinarySensorEntityDescription( FILTER_CLEAN_REQUIRED_DESCRIPTION = SensiboDeviceBinarySensorEntityDescription( key="filter_clean", device_class=BinarySensorDeviceClass.PROBLEM, - name="Filter Clean Required", + name="Filter clean required", value_fn=lambda data: data.filter_clean, ) @@ -71,7 +71,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( SensiboMotionBinarySensorEntityDescription( key="is_main_sensor", entity_category=EntityCategory.DIAGNOSTIC, - name="Main Sensor", + name="Main sensor", icon="mdi:connection", value_fn=lambda data: data.is_main_sensor, ), @@ -88,7 +88,7 @@ MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, .. SensiboDeviceBinarySensorEntityDescription( key="room_occupied", device_class=BinarySensorDeviceClass.MOTION, - name="Room Occupied", + name="Room occupied", icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, ), @@ -111,7 +111,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( key="pure_geo_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with Presence", + name="Pure Boost linked with presence", icon="mdi:connection", value_fn=lambda data: data.pure_geo_integration, ), @@ -119,7 +119,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( key="pure_measure_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with Indoor Air Quality", + name="Pure Boost linked with indoor air quality", icon="mdi:connection", value_fn=lambda data: data.pure_measure_integration, ), @@ -127,7 +127,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( key="pure_prime_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with Outdoor Air Quality", + name="Pure Boost linked with outdoor air quality", icon="mdi:connection", value_fn=lambda data: data.pure_prime_integration, ), @@ -194,13 +194,9 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, BinarySensorEntity): device_id, sensor_id, sensor_data, - entity_description.name, ) self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" - self._attr_name = ( - f"{self.device_data.name} Motion Sensor {entity_description.name}" - ) @property def is_on(self) -> bool | None: @@ -228,7 +224,6 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, BinarySensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/sensibo/button.py b/homeassistant/components/sensibo/button.py index 97ae6321f7e..ad8a525aebb 100644 --- a/homeassistant/components/sensibo/button.py +++ b/homeassistant/components/sensibo/button.py @@ -16,7 +16,7 @@ PARALLEL_UPDATES = 0 DEVICE_BUTTON_TYPES: ButtonEntityDescription = ButtonEntityDescription( key="reset_filter", - name="Reset Filter", + name="Reset filter", icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, ) @@ -57,7 +57,6 @@ class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index b4af38ab69c..25ac73bfd4f 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -126,7 +126,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Initiate SensiboClimate.""" super().__init__(coordinator, device_id) self._attr_unique_id = device_id - self._attr_name = self.device_data.name self._attr_temperature_unit = ( TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT ) diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index ac2ec24fac1..41d8b8b5070 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -37,6 +37,8 @@ class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): class SensiboDeviceBaseEntity(SensiboBaseEntity): """Representation of a Sensibo device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SensiboDataUpdateCoordinator, @@ -114,21 +116,21 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): class SensiboMotionBaseEntity(SensiboBaseEntity): """Representation of a Sensibo motion entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str, sensor_id: str, sensor_data: MotionSensor, - name: str | None, ) -> None: """Initiate Sensibo Number.""" super().__init__(coordinator, device_id) self._sensor_id = sensor_id - self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, sensor_id)}, - name=f"{self.device_data.name} Motion Sensor {name}", + name=f"{self.device_data.name} Motion Sensor", via_device=(DOMAIN, device_id), manufacturer="Sensibo", configuration_url="https://home.sensibo.com/", diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 18e93d5efa6..5ef8ff6fa4e 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.17"], + "requirements": ["pysensibo==1.0.18"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 183c4db4b87..bcad658c700 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -86,7 +86,6 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def native_value(self) -> float | None: diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index f64411ff4dc..a8cfc527704 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -36,7 +36,7 @@ DEVICE_SELECT_TYPES = ( key="horizontalSwing", remote_key="horizontal_swing_mode", remote_options="horizontal_swing_modes", - name="Horizontal Swing", + name="Horizontal swing", icon="mdi:air-conditioner", ), SensiboSelectEntityDescription( @@ -79,7 +79,6 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def current_option(self) -> str | None: diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index fad22fdd677..f21366c7aa6 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -17,10 +17,13 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, ELECTRIC_POTENTIAL_VOLT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory @@ -66,7 +69,7 @@ class SensiboDeviceSensorEntityDescription( FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription( key="filter_last_reset", device_class=SensorDeviceClass.TIMESTAMP, - name="Filter Last Reset", + name="Filter last reset", icon="mdi:timer", value_fn=lambda data: data.filter_last_reset, extra_fn=None, @@ -90,7 +93,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=SensorStateClass.MEASUREMENT, - name="Battery Voltage", + name="Battery voltage", icon="mdi:battery", value_fn=lambda data: data.battery_voltage, ), @@ -106,7 +109,6 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( SensiboMotionSensorEntityDescription( key="temperature", device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, name="Temperature", icon="mdi:thermometer", @@ -126,7 +128,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( ), SensiboDeviceSensorEntityDescription( key="pure_sensitivity", - name="Pure Sensitivity", + name="Pure sensitivity", icon="mdi:air-filter", value_fn=lambda data: data.pure_sensitivity, extra_fn=None, @@ -138,14 +140,44 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="timer_time", device_class=SensorDeviceClass.TIMESTAMP, - name="Timer End Time", + name="Timer end time", icon="mdi:timer", value_fn=lambda data: data.timer_time, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), + SensiboDeviceSensorEntityDescription( + key="feels_like", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + name="Temperature feels like", + value_fn=lambda data: data.feelslike, + extra_fn=None, + entity_registry_enabled_default=False, + ), FILTER_LAST_RESET_DESCRIPTION, ) +AIRQ_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( + SensiboDeviceSensorEntityDescription( + key="airq_tvoc", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:air-filter", + name="AirQ TVOC", + value_fn=lambda data: data.tvoc, + extra_fn=None, + ), + SensiboDeviceSensorEntityDescription( + key="airq_co2", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + name="AirQ CO2", + value_fn=lambda data: data.co2, + extra_fn=None, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -177,6 +209,12 @@ async def async_setup_entry( for description in DEVICE_SENSOR_TYPES if device_data.model != "pure" ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for device_id, device_data in coordinator.data.parsed.items() + for description in AIRQ_SENSOR_TYPES + if device_data.model == "airq" + ) async_add_entities(entities) @@ -199,13 +237,18 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): device_id, sensor_id, sensor_data, - entity_description.name, ) self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" - self._attr_name = ( - f"{self.device_data.name} Motion Sensor {entity_description.name}" - ) + + @property + def native_unit_of_measurement(self) -> str | None: + """Add native unit of measurement.""" + if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: + return ( + TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT + ) + return self.entity_description.native_unit_of_measurement @property def native_value(self) -> StateType: @@ -233,7 +276,15 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" + + @property + def native_unit_of_measurement(self) -> str | None: + """Add native unit of measurement.""" + if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: + return ( + TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT + ) + return self.entity_description.native_unit_of_measurement @property def native_value(self) -> StateType | datetime: diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index d9cf9417504..14cfaac73ae 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -135,7 +135,6 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/sensibo/translations/pt.json b/homeassistant/components/sensibo/translations/pt.json new file mode 100644 index 00000000000..80f65d0a06d --- /dev/null +++ b/homeassistant/components/sensibo/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.ar.json b/homeassistant/components/sensibo/translations/sensor.ar.json new file mode 100644 index 00000000000..b8d510999fd --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.ar.json @@ -0,0 +1,7 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u0637\u0628\u064a\u0639\u064a\u0627" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.ru.json b/homeassistant/components/sensibo/translations/sensor.ru.json new file mode 100644 index 00000000000..c916a2b22f4 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.ru.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439", + "s": "\u0427\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/update.py b/homeassistant/components/sensibo/update.py index 48304cbd3c5..67e0cbd6e65 100644 --- a/homeassistant/components/sensibo/update.py +++ b/homeassistant/components/sensibo/update.py @@ -43,7 +43,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceUpdateEntityDescription, ...] = ( key="fw_ver_available", device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.DIAGNOSTIC, - name="Update Available", + name="Update available", icon="mdi:rocket-launch", value_version=lambda data: data.fw_ver, value_available=lambda data: data.fw_ver_available, @@ -81,7 +81,6 @@ class SensiboDeviceUpdate(SensiboDeviceBaseEntity, UpdateEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" self._attr_title = self.device_data.model @property diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 3fc5cbec7ee..ea7d129a9c3 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -275,7 +275,7 @@ def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: custom_component = entity_sources(hass).get(entity_id, {}).get("custom_component") report_issue = "" if custom_component: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index f493f4134ed..f6f2e97fdd4 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -11,6 +11,7 @@ "is_power_factor": "Aktu\u00e1ln\u00ed \u00fa\u010din\u00edk {entity_name}", "is_pressure": "Aktu\u00e1ln\u00ed tlak {entity_name}", "is_signal_strength": "Aktu\u00e1ln\u00ed s\u00edla sign\u00e1lu {entity_name}", + "is_sulphur_dioxide": "Aktu\u00e1ln\u00ed \u00farove\u0148 koncentrace oxidu si\u0159i\u010dit\u00e9ho {entity_name}", "is_temperature": "Aktu\u00e1ln\u00ed teplota {entity_name}", "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}", "is_voltage": "Aktu\u00e1ln\u00ed nap\u011bt\u00ed {entity_name}" @@ -22,6 +23,7 @@ "gas": "P\u0159i zm\u011bn\u011b mno\u017estv\u00ed plynu {entity_name}", "humidity": "P\u0159i zm\u011bn\u011b vlhkosti {entity_name}", "illuminance": "P\u0159i zm\u011bn\u011b osv\u011btlen\u00ed {entity_name}", + "nitrogen_monoxide": "Zm\u011bna koncentrace oxidu dusnat\u00e9ho {entity_name}", "power": "P\u0159i zm\u011bn\u011b el. v\u00fdkonu {entity_name}", "power_factor": "P\u0159i zm\u011bn\u011b \u00fa\u010din\u00edku {entity_name}", "pressure": "P\u0159i zm\u011bn\u011b tlaku {entity_name}", diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index 819da5aad0c..7f2dd33a023 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -2,10 +2,12 @@ "device_automation": { "condition_type": { "is_apparent_power": "\u05d4\u05e2\u05d5\u05e6\u05de\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} \u05de\u05e1\u05ea\u05de\u05e0\u05ea", + "is_battery_level": "\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea \u05e9\u05dc {entity_name}", "is_reactive_power": "\u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}" }, "trigger_type": { "apparent_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05dc\u05db\u05d0\u05d5\u05e8\u05d4", + "battery_level": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4", "reactive_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9" } }, diff --git a/homeassistant/components/sensor/translations/pt.json b/homeassistant/components/sensor/translations/pt.json index eaef8ef5755..864d49f2373 100644 --- a/homeassistant/components/sensor/translations/pt.json +++ b/homeassistant/components/sensor/translations/pt.json @@ -2,6 +2,7 @@ "device_automation": { "condition_type": { "is_battery_level": "N\u00edvel de bateria atual de {entity_name}", + "is_energy": "Energia atual de {entity_name}", "is_humidity": "humidade {entity_name}", "is_illuminance": "Luminancia atual de {entity_name}", "is_power": "Pot\u00eancia atual de {entity_name}", @@ -12,11 +13,12 @@ }, "trigger_type": { "battery_level": "n\u00edvel da bateria {entity_name}", + "energy": "Mudan\u00e7as de energia de {entity_name}", "humidity": "humidade {entity_name}", "illuminance": "ilumin\u00e2ncia {entity_name}", "power": "pot\u00eancia {entity_name}", "pressure": "press\u00e3o {entity_name}", - "signal_strength": "for\u00e7a do sinal de {entity_name}", + "signal_strength": "Altera\u00e7\u00e3o da intensidade do sinal de {entity_name}", "temperature": "temperatura de {entity_name}", "value": "valor {entity_name}" } diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py new file mode 100644 index 00000000000..d4a0872ba3f --- /dev/null +++ b/homeassistant/components/sensorpush/__init__.py @@ -0,0 +1,42 @@ +"""The SensorPush Bluetooth integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up SensorPush BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py new file mode 100644 index 00000000000..d10c2f481a6 --- /dev/null +++ b/homeassistant/components/sensorpush/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for sensorpush integration.""" +from __future__ import annotations + +from typing import Any + +from sensorpush_ble import SensorPushBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for sensorpush.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/sensorpush/const.py b/homeassistant/components/sensorpush/const.py new file mode 100644 index 00000000000..8f566c72d07 --- /dev/null +++ b/homeassistant/components/sensorpush/const.py @@ -0,0 +1,3 @@ +"""Constants for the SensorPush Bluetooth integration.""" + +DOMAIN = "sensorpush" diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json new file mode 100644 index 00000000000..a5d900aaf3b --- /dev/null +++ b/homeassistant/components/sensorpush/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "sensorpush", + "name": "SensorPush", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sensorpush", + "bluetooth": [ + { + "local_name": "SensorPush*" + } + ], + "requirements": ["sensorpush-ble==1.5.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py new file mode 100644 index 00000000000..9bfa59e3876 --- /dev/null +++ b/homeassistant/components/sensorpush/sensor.py @@ -0,0 +1,158 @@ +"""Support for sensorpush ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from sensorpush_ble import ( + DeviceClass, + DeviceKey, + SensorDeviceInfo, + SensorPushBluetoothDeviceData, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SensorPush BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + data = SensorPushBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload( + processor.async_add_entities_listener( + SensorPushBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class SensorPushBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a sensorpush ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/sensorpush/strings.json b/homeassistant/components/sensorpush/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/sensorpush/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "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%]" + } + } +} diff --git a/homeassistant/components/sensorpush/translations/ca.json b/homeassistant/components/sensorpush/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/sensorpush/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/de.json b/homeassistant/components/sensorpush/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/el.json b/homeassistant/components/sensorpush/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/en.json b/homeassistant/components/sensorpush/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/et.json b/homeassistant/components/sensorpush/translations/et.json new file mode 100644 index 00000000000..94bc17992fe --- /dev/null +++ b/homeassistant/components/sensorpush/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", + "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/fr.json b/homeassistant/components/sensorpush/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/sensorpush/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/hu.json b/homeassistant/components/sensorpush/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/id.json b/homeassistant/components/sensorpush/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/it.json b/homeassistant/components/sensorpush/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/ja.json b/homeassistant/components/sensorpush/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/pl.json b/homeassistant/components/sensorpush/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/pt-BR.json b/homeassistant/components/sensorpush/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/ru.json b/homeassistant/components/sensorpush/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/zh-Hant.json b/homeassistant/components/sensorpush/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/sensorpush/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 3f01b7bc5f0..f6567fbd04d 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.6.0"], + "requirements": ["sentry-sdk==1.8.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sentry/translations/ja.json b/homeassistant/components/sentry/translations/ja.json index 8ac8ebf58a1..56312cecda3 100644 --- a/homeassistant/components/sentry/translations/ja.json +++ b/homeassistant/components/sentry/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "bad_dsn": "\u7121\u52b9\u306aDSN", diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 08aa26fa3c5..9155c8ca036 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -6,14 +6,10 @@ import logging from aiosenz import SENZAPI, Thermostat from httpx import RequestError -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( @@ -31,20 +27,7 @@ UPDATE_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.CLIMATE] @@ -52,27 +35,17 @@ SENZDataUpdateCoordinator = DataUpdateCoordinator[dict[str, Thermostat]] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the SENZ OAuth2 configuration.""" - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of SENZ integration in YAML is deprecated " - "and will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) + """Set up the SENZ integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) return True @@ -111,9 +84,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/senz/manifest.json b/homeassistant/components/senz/manifest.json index 937a20d8482..36687e46d4a 100644 --- a/homeassistant/components/senz/manifest.json +++ b/homeassistant/components/senz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/senz", "requirements": ["aiosenz==1.0.0"], - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "codeowners": ["@milanmeu"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/senz/strings.json b/homeassistant/components/senz/strings.json index 316f7234f9b..74ca9f5e3bf 100644 --- a/homeassistant/components/senz/strings.json +++ b/homeassistant/components/senz/strings.json @@ -16,5 +16,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "removed_yaml": { + "title": "The nVent RAYCHEM SENZ YAML configuration has been removed", + "description": "Configuring nVent RAYCHEM SENZ using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/senz/translations/de.json b/homeassistant/components/senz/translations/de.json index ffbc7bb458f..fbae91321be 100644 --- a/homeassistant/components/senz/translations/de.json +++ b/homeassistant/components/senz/translations/de.json @@ -16,5 +16,11 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von nVent RAYCHEM SENZ mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die nVent RAYCHEM SENZ YAML Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/el.json b/homeassistant/components/senz/translations/el.json index cd34896da39..b6ae9a85e99 100644 --- a/homeassistant/components/senz/translations/el.json +++ b/homeassistant/components/senz/translations/el.json @@ -16,5 +16,11 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" } } + }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 nVent RAYCHEM SENZ \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd YAML \u03c4\u03bf\u03c5 nVent RAYCHEM SENZ \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/en.json b/homeassistant/components/senz/translations/en.json index bdf574691c5..fc1cfd561d4 100644 --- a/homeassistant/components/senz/translations/en.json +++ b/homeassistant/components/senz/translations/en.json @@ -16,5 +16,11 @@ "title": "Pick Authentication Method" } } + }, + "issues": { + "removed_yaml": { + "description": "Configuring nVent RAYCHEM SENZ using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The nVent RAYCHEM SENZ YAML configuration has been removed" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/it.json b/homeassistant/components/senz/translations/it.json index c92ebb2a57c..be6608af499 100644 --- a/homeassistant/components/senz/translations/it.json +++ b/homeassistant/components/senz/translations/it.json @@ -16,5 +16,11 @@ "title": "Scegli il metodo di autenticazione" } } + }, + "issues": { + "removed_yaml": { + "description": "La configurazione di nVent RAYCHEM SENZ tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di nVent RAYCHEM SENZ \u00e8 stata rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pl.json b/homeassistant/components/senz/translations/pl.json index d58148cb8fa..79fd5e3a29c 100644 --- a/homeassistant/components/senz/translations/pl.json +++ b/homeassistant/components/senz/translations/pl.json @@ -16,5 +16,11 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja nVent RAYCHEM SENZ za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla nVent RAYCHEM SENZ zosta\u0142a usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pt-BR.json b/homeassistant/components/senz/translations/pt-BR.json index 7e3ff2f64a9..02c31d97816 100644 --- a/homeassistant/components/senz/translations/pt-BR.json +++ b/homeassistant/components/senz/translations/pt-BR.json @@ -16,5 +16,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do nVent RAYCHEM SENZ usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o nVent RAYCHEM SENZ YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/zh-Hant.json b/homeassistant/components/senz/translations/zh-Hant.json index 3bf08cf34c7..7094ee18a02 100644 --- a/homeassistant/components/senz/translations/zh-Hant.json +++ b/homeassistant/components/senz/translations/zh-Hant.json @@ -16,5 +16,11 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a nVent RAYCHEM SENZ \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "nVent RAYCHEM SENZ YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index caee1d72c38..09457fc4ca5 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==9.1.1"], + "requirements": ["pillow==9.2.0"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index aad05652b0b..0c4f7bb0bfc 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 012f692c579..125e63449ef 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine from datetime import timedelta from typing import Any, Final, cast @@ -296,7 +297,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device - self._debounced_reload = Debouncer( + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, @@ -636,7 +637,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device - self._debounced_reload = Debouncer( + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 18f53ca232d..71a9e96126b 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "firmware_not_fully_provisioned": "Az eszk\u00f6z nincs teljesen be\u00fczemelve. K\u00e9rj\u00fck, vegye fel a kapcsolatot a Shelly \u00fcgyf\u00e9lszolg\u00e1lat\u00e1val", + "firmware_not_fully_provisioned": "Az eszk\u00f6z be\u00fczemel\u00e9se nem teljes. K\u00e9rem, vegye fel a kapcsolatot a Shelly \u00fcgyf\u00e9lszolg\u00e1lat\u00e1val", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/shelly/translations/pt.json b/homeassistant/components/shelly/translations/pt.json index d66cc0e5dd9..43f18a5c9d6 100644 --- a/homeassistant/components/shelly/translations/pt.json +++ b/homeassistant/components/shelly/translations/pt.json @@ -25,5 +25,10 @@ } } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Bot\u00e3o" + } } } \ No newline at end of file diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 7d609a5e9c3..98af966c496 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,7 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": ["shodan==1.27.0"], + "requirements": ["shodan==1.28.0"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling", "loggers": ["shodan"] diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 5f6a13e8e13..2af54722739 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -335,7 +335,11 @@ class ClearCompletedItemsView(http.HomeAssistantView): @callback -def websocket_handle_items(hass, connection, msg): +def websocket_handle_items( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle get shopping_list items.""" connection.send_message( websocket_api.result_message(msg["id"], hass.data[DOMAIN].items) @@ -343,15 +347,25 @@ def websocket_handle_items(hass, connection, msg): @websocket_api.async_response -async def websocket_handle_add(hass, connection, msg): +async def websocket_handle_add( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle add item to shopping_list.""" item = await hass.data[DOMAIN].async_add(msg["name"]) - hass.bus.async_fire(EVENT, {"action": "add", "item": item}) + hass.bus.async_fire( + EVENT, {"action": "add", "item": item}, context=connection.context(msg) + ) connection.send_message(websocket_api.result_message(msg["id"], item)) @websocket_api.async_response -async def websocket_handle_update(hass, connection, msg): +async def websocket_handle_update( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle update shopping_list item.""" msg_id = msg.pop("id") item_id = msg.pop("item_id") @@ -360,7 +374,9 @@ async def websocket_handle_update(hass, connection, msg): try: item = await hass.data[DOMAIN].async_update(item_id, data) - hass.bus.async_fire(EVENT, {"action": "update", "item": item}) + hass.bus.async_fire( + EVENT, {"action": "update", "item": item}, context=connection.context(msg) + ) connection.send_message(websocket_api.result_message(msg_id, item)) except KeyError: connection.send_message( @@ -369,10 +385,14 @@ async def websocket_handle_update(hass, connection, msg): @websocket_api.async_response -async def websocket_handle_clear(hass, connection, msg): +async def websocket_handle_clear( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle clearing shopping_list items.""" await hass.data[DOMAIN].async_clear_completed() - hass.bus.async_fire(EVENT, {"action": "clear"}) + hass.bus.async_fire(EVENT, {"action": "clear"}, context=connection.context(msg)) connection.send_message(websocket_api.result_message(msg["id"])) @@ -382,12 +402,18 @@ async def websocket_handle_clear(hass, connection, msg): vol.Required("item_ids"): [str], } ) -def websocket_handle_reorder(hass, connection, msg): +def websocket_handle_reorder( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle reordering shopping_list items.""" msg_id = msg.pop("id") try: hass.data[DOMAIN].async_reorder(msg.pop("item_ids")) - hass.bus.async_fire(EVENT, {"action": "reorder"}) + hass.bus.async_fire( + EVENT, {"action": "reorder"}, context=connection.context(msg) + ) connection.send_result(msg_id) except KeyError: connection.send_error( diff --git a/homeassistant/components/shopping_list/translations/pt.json b/homeassistant/components/shopping_list/translations/pt.json index 9e8b24efa29..bdb2d4041ef 100644 --- a/homeassistant/components/shopping_list/translations/pt.json +++ b/homeassistant/components/shopping_list/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A lista de compras j\u00e1 est\u00e1 configurada." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "step": { "user": { diff --git a/homeassistant/components/sia/__init__.py b/homeassistant/components/sia/__init__.py index dbbb12f29ce..31ae36f793d 100644 --- a/homeassistant/components/sia/__init__.py +++ b/homeassistant/components/sia/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"SIA Server at port {entry.data[CONF_PORT]} could not start." ) from exc - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index 4f46e88162c..399da14c2ad 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -141,4 +141,4 @@ class SIAHub: return hub.update_accounts() await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) diff --git a/homeassistant/components/sia/translations/hu.json b/homeassistant/components/sia/translations/hu.json index 63a5208a44c..cd3bf4a9578 100644 --- a/homeassistant/components/sia/translations/hu.json +++ b/homeassistant/components/sia/translations/hu.json @@ -1,9 +1,9 @@ { "config": { "error": { - "invalid_account_format": "A sz\u00e1mla nem hexa\u00e9rt\u00e9k, k\u00e9rj\u00fck, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", + "invalid_account_format": "A megadott fi\u00f3k nem hexa\u00e9rt\u00e9k, k\u00e9rem, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", "invalid_account_length": "A fi\u00f3k nem megfelel\u0151 hossz\u00fas\u00e1g\u00fa, 3 \u00e9s 16 karakter k\u00f6z\u00f6tt kell lennie.", - "invalid_key_format": "A kulcs nem hexa\u00e9rt\u00e9k, k\u00e9rj\u00fck, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", + "invalid_key_format": "A megadott kulcs nem hexa\u00e9rt\u00e9k, k\u00e9rem, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", "invalid_key_length": "A kulcs nem megfelel\u0151 hossz\u00fas\u00e1g\u00fa, 16, 24 vagy 32 hexa karakterb\u0151l kell \u00e1llnia.", "invalid_ping": "A ping intervallumnak 1 \u00e9s 1440 perc k\u00f6z\u00f6tt kell lennie.", "invalid_zones": "Legal\u00e1bb 1 z\u00f3n\u00e1nak kell lennie.", diff --git a/homeassistant/components/sia/translations/pt.json b/homeassistant/components/sia/translations/pt.json new file mode 100644 index 00000000000..21f8130bfc5 --- /dev/null +++ b/homeassistant/components/sia/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignorar a verifica\u00e7\u00e3o de carimbo de data/hora dos eventos SIA", + "zones": "N\u00famero de zonas da conta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index e87e1d37304..400664079e2 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==9.1.1", "simplehound==0.3"], + "requirements": ["pillow==9.2.0", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling", "loggers": ["simplehound"] diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index 7c37546485a..6b4ee263ba6 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -5,6 +5,7 @@ "requirements": ["simplepush==1.1.4"], "codeowners": ["@engrbm87"], "config_flow": true, + "dependencies": ["repairs"], "iot_class": "cloud_polling", "loggers": ["simplepush"] } diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index e9cd9813175..2e58748f323 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -14,6 +14,8 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.components.notify.const import ATTR_DATA +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_EVENT, CONF_PASSWORD from homeassistant.core import HomeAssistant @@ -42,6 +44,15 @@ async def async_get_service( ) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" if discovery_info is None: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config diff --git a/homeassistant/components/simplepush/strings.json b/homeassistant/components/simplepush/strings.json index 0031dc32340..77ed05c4b48 100644 --- a/homeassistant/components/simplepush/strings.json +++ b/homeassistant/components/simplepush/strings.json @@ -17,5 +17,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/simplepush/translations/ar.json b/homeassistant/components/simplepush/translations/ar.json new file mode 100644 index 00000000000..81b5a92d394 --- /dev/null +++ b/homeassistant/components/simplepush/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0647\u0627\u0632 \u062a\u0645 \u062a\u0647\u064a\u0626\u062a\u0647 \u0645\u0646 \u0642\u0628\u0644 " + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "device_key": "\u0627\u0644\u0645\u0641\u062a\u0627\u062d \u0627\u0644\u062e\u0627\u0635 \u0628\u062c\u0647\u0627\u0632\u0643", + "name": "\u0627\u0644\u0627\u0633\u0645" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index a36a3b2b273..bf373d8baf0 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index 900bac61bc5..176318b3f3c 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/simplepush/translations/pl.json b/homeassistant/components/simplepush/translations/pl.json new file mode 100644 index 00000000000..fe19feb39a1 --- /dev/null +++ b/homeassistant/components/simplepush/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "device_key": "Klucz Twojego urz\u0105dzenia", + "event": "Wydarzenie dla wydarze\u0144.", + "name": "Nazwa", + "password": "Has\u0142o szyfrowania u\u017cywane przez Twoje urz\u0105dzenie", + "salt": "Salt u\u017cywane przez Twoje urz\u0105dzenie." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pt.json b/homeassistant/components/simplepush/translations/pt.json new file mode 100644 index 00000000000..d7e598b33e4 --- /dev/null +++ b/homeassistant/components/simplepush/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json new file mode 100644 index 00000000000..4844f358c82 --- /dev/null +++ b/homeassistant/components/simplepush/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "device_key": "\u041a\u043b\u044e\u0447 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "event": "\u0421\u043e\u0431\u044b\u0442\u0438\u0435 \u0434\u043b\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u0412\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c", + "salt": "\u0421\u043e\u043b\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/tr.json b/homeassistant/components/simplepush/translations/tr.json new file mode 100644 index 00000000000..0c969465da9 --- /dev/null +++ b/homeassistant/components/simplepush/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "device_key": "Cihaz\u0131n\u0131z\u0131n cihaz anahtar\u0131", + "event": "Olaylar i\u00e7in olay.", + "name": "Ad", + "password": "Cihaz\u0131n\u0131z taraf\u0131ndan kullan\u0131lan \u015fifreleme parolas\u0131", + "salt": "Cihaz\u0131n\u0131z taraf\u0131ndan kullan\u0131lan tuz." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 660f45b355d..c28a3740694 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -113,8 +113,7 @@ ATTR_SYSTEM_ID = "system_id" ATTR_TIMESTAMP = "timestamp" DEFAULT_CONFIG_URL = "https://webapp.simplisafe.com/new/#/dashboard" -DEFAULT_ENTITY_MODEL = "alarm_control_panel" -DEFAULT_ENTITY_NAME = "Alarm Control Panel" +DEFAULT_ENTITY_MODEL = "Alarm control panel" DEFAULT_ERROR_THRESHOLD = 2 DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 @@ -325,7 +324,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = simplisafe - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def extract_system(func: Callable) -> Callable: @@ -658,6 +657,8 @@ class SimpliSafe: class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" + _attr_has_entity_name = True + def __init__( self, simplisafe: SimpliSafe, @@ -677,12 +678,11 @@ class SimpliSafeEntity(CoordinatorEntity): self._error_count = 0 if device: - model = device.type.name - device_name = device.name + model = device.type.name.capitalize().replace("_", " ") + device_name = f"{device.name.capitalize()} {model}" serial = device.serial else: - model = DEFAULT_ENTITY_MODEL - device_name = DEFAULT_ENTITY_NAME + model = device_name = DEFAULT_ENTITY_MODEL serial = system.serial event = simplisafe.initial_event_to_use[system.system_id] @@ -712,7 +712,6 @@ class SimpliSafeEntity(CoordinatorEntity): via_device=(DOMAIN, system.system_id), ) - self._attr_name = f"{system.address} {device_name} {' '.join([w.title() for w in model.split('_')])}" self._attr_unique_id = serial self._device = device self._online = True diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index 3e4c64e8658..239f5468cb3 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -103,7 +103,7 @@ class BatteryBinarySensor(SimpliSafeEntity, BinarySensorEntity): """Initialize.""" super().__init__(simplisafe, system, device=sensor) - self._attr_name = f"{super().name} Battery" + self._attr_name = "Battery" self._attr_unique_id = f"{super().unique_id}-battery" self._device: SensorV3 diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index a2fd356932c..dbb3d0e4207 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Aquest compte SimpliSafe ja est\u00e0 en \u00fas.", "email_2fa_timed_out": "S'ha esgotat el temps d'espera de l'autenticaci\u00f3 de dos factors a trav\u00e9s de correu electr\u00f2nic.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "wrong_account": "Les credencials d'usuari proporcionades no coincideixen amb les d'aquest compte SimpliSafe." }, "error": { + "identifier_exists": "Compte ja est\u00e0 registrat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Codi d'autoritzaci\u00f3", "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Introdueix el teu nom d'usuari i contrasenya." + "description": "SimpliSafe autentica els seus usuaris a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) abans de comen\u00e7ar.\n\nQuan ja estiguis, fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials. Quan el proc\u00e9s s'hagi completat, torna aqu\u00ed i introdueix, a sota, el codi d'autoritzaci\u00f3 de l'URL de l'aplicaci\u00f3 web de SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index d224f3c2441..4788f2201b4 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet.", "email_2fa_timed_out": "Zeit\u00fcberschreitung beim Warten auf E-Mail-basierte Zwei-Faktor-Authentifizierung.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "wrong_account": "Die angegebenen Benutzeranmeldeinformationen stimmen nicht mit diesem SimpliSafe-Konto \u00fcberein." }, "error": { + "identifier_exists": "Konto bereits registriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Autorisierungscode", "password": "Passwort", "username": "Benutzername" }, - "description": "Gib deinen Benutzernamen und Passwort ein." + "description": "SimpliSafe authentifiziert Benutzer \u00fcber seine Web-App. Aufgrund technischer Einschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; Bitte stelle sicher, dass Du die [Dokumentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) lesen, bevor Sie beginnen. \n\n Wenn Sie fertig sind, klicke [hier]({url}), um die SimpliSafe-Web-App zu \u00f6ffnen und Ihre Anmeldeinformationen einzugeben. Wenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gebe den Autorisierungscode von der SimpliSafe-Web-App-URL ein." } } }, diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index d852664ce38..ecc263e6821 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 SimpliSafe \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7.", "email_2fa_timed_out": "\u03a4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03ad\u03bb\u03b7\u03be\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 email.", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "wrong_account": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc SimpliSafe." }, "error": { + "identifier_exists": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, @@ -28,6 +30,7 @@ }, "user": { "data": { + "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "Email" }, diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 82320df4864..70b0cc15383 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", + "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, @@ -10,10 +11,28 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, + "progress": { + "email_2fa": "Check your email for a verification link from Simplisafe." + }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please re-enter the password for {username}.", + "title": "Reauthenticate Integration" + }, + "sms_2fa": { + "data": { + "code": "Code" + }, + "description": "Input the two-factor authentication code sent to you via SMS." + }, "user": { "data": { - "auth_code": "Authorization Code" + "auth_code": "Authorization Code", + "password": "Password", + "username": "Username" }, "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL." } diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 0f01f4d9b9c..073d20555c1 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "See SimpliSafe'i konto on juba kasutusel.", "email_2fa_timed_out": "Meilip\u00f5hise kahefaktorilise autentimise ajal\u00f5pp.", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "wrong_account": "Esitatud kasutaja mandaadid ei \u00fchti selle SimpliSafe kontoga." }, "error": { + "identifier_exists": "Konto on juba registreeritud", "invalid_auth": "Tuvastamise viga", "unknown": "Tundmatu viga" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Tuvastuskood", "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "Sisesta kasutajatunnus ja salas\u00f5na" + "description": "SimpliSafe autendib kasutajaid oma veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus k\u00e4sitsi samm; palun veendu, et loed enne alustamist l\u00e4bi [dokumendid](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKui oled valmis, kl\u00f5psa veebirakenduse SimpliSafe avamiseks ja mandaadi sisestamiseks kl\u00f5psa [here]({url}). Kui protsess on l\u00f5pule j\u00f5udnud, naase siia ja sisesta autoriseerimiskood SimpliSafe veebirakenduse URL-ist." } } }, diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 4de2af95885..15ad31ce463 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9.", "email_2fa_timed_out": "D\u00e9lai d'attente de l'authentification \u00e0 deux facteurs par courriel expir\u00e9.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "wrong_account": "Les informations d'identification d'utilisateur fournies ne correspondent pas \u00e0 ce compte SimpliSafe." }, "error": { + "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, @@ -28,10 +30,10 @@ }, "user": { "data": { + "auth_code": "Code d'autorisation", "password": "Mot de passe", "username": "Nom d'utilisateur" - }, - "description": "Saisissez votre nom d'utilisateur et votre mot de passe." + } } } }, diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 12f745b3b69..ece2b0a0dfb 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Ez a SimpliSafe-fi\u00f3k m\u00e1r haszn\u00e1latban van.", "email_2fa_timed_out": "Az e-mail alap\u00fa k\u00e9tfaktoros hiteles\u00edt\u00e9sre val\u00f3 v\u00e1rakoz\u00e1s ideje lej\u00e1rt", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "wrong_account": "A megadott felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok nem j\u00f3k ehhez a SimpliSafe fi\u00f3khoz." }, "error": { + "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lt", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, @@ -28,6 +30,7 @@ }, "user": { "data": { + "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index b9caae3f0a4..ba75954ce71 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Questo account SimpliSafe \u00e8 gi\u00e0 in uso.", "email_2fa_timed_out": "Timeout durante l'attesa dell'autenticazione a due fattori basata su email.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "wrong_account": "Le credenziali utente fornite non corrispondono a questo account SimpliSafe." }, "error": { + "identifier_exists": "Account gi\u00e0 registrato", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Codice di autorizzazione", "password": "Password", "username": "Nome utente" }, - "description": "Digita il tuo nome utente e password." + "description": "SimpliSafe autentica gli utenti tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati, prima di iniziare, di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\nQuando sei pronto, fai clic [qui]({url}) per aprire l'app Web SimpliSafe e inserire le tue credenziali. Al termine del processo, ritorna qui e inserisci il codice di autorizzazione dall'URL dell'app Web SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 57909d30f69..e264ec18aab 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -18,7 +18,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30a2\u30af\u30bb\u30b9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u53d6\u308a\u6d88\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u30ea\u30f3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "sms_2fa": { "data": { diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 22c4739b7dd..04f6e8dd44b 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu", "email_2fa_timed_out": "Przekroczono limit czasu oczekiwania na e-mailowe uwierzytelnianie dwusk\u0142adnikowe.", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "wrong_account": "Podane dane uwierzytelniaj\u0105ce u\u017cytkownika nie pasuj\u0105 do tego konta SimpliSafe." }, "error": { + "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Kod autoryzacji", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o." + "description": "SimpliSafe uwierzytelnia u\u017cytkownik\u00f3w za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) przed rozpocz\u0119ciem. \n\nGdy b\u0119dziesz ju\u017c gotowy, kliknij [tutaj]({url}), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Po zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i wprowad\u017a kod autoryzacji z adresu URL aplikacji internetowej SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index ccfb13b6cc1..74b30d2a9ef 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "A conta j\u00e1 foi configurada", "email_2fa_timed_out": "Expirou enquanto aguardava a autentica\u00e7\u00e3o de dois fatores enviada por e-mail.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "wrong_account": "As credenciais de usu\u00e1rio fornecidas n\u00e3o correspondem a esta conta SimpliSafe." }, "error": { + "identifier_exists": "Conta j\u00e1 cadastrada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "C\u00f3digo de autoriza\u00e7\u00e3o", "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "Insira seu nome de usu\u00e1rio e senha." + "description": "O SimpliSafe autentica os usu\u00e1rios por meio de seu aplicativo da web. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de come\u00e7ar. \n\n Quando estiver pronto, clique [aqui]( {url} ) para abrir o aplicativo Web SimpliSafe e insira suas credenciais. Quando o processo estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o da URL do aplicativo Web SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 198d5225983..1b88417dd27 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "email_2fa_timed_out": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "wrong_account": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 SimpliSafe." }, "error": { + "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." + "description": "SimpliSafe \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u0441\u0432\u043e\u0435 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 [\u0441\u044e\u0434\u0430]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index ce763da538e..99f08eb14d4 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u6b64 SimpliSafe \u5e33\u865f\u5df2\u88ab\u4f7f\u7528\u3002", "email_2fa_timed_out": "\u7b49\u5f85\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u90f5\u4ef6\u903e\u6642\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "wrong_account": "\u6240\u4ee5\u63d0\u4f9b\u7684\u6191\u8b49\u8207 Simplisafe \u5e33\u865f\u4e0d\u7b26\u3002" }, "error": { + "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "\u8a8d\u8b49\u78bc", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002" + "description": "SimpliSafe \u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u5176\u4f7f\u7528\u8005\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3002\n\n\u6e96\u5099\u5c31\u7dd2\u5f8c\u3001\u9ede\u9078 [\u6b64\u8655]({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\u5b8c\u6210\u5f8c\u56de\u5230\u9019\u88e1\u4e26\u8f38\u5165\u7531 SimpliSafe Web App \u6240\u53d6\u7684\u8a8d\u8b49\u78bc\u3002" } } }, diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 00c7a533590..9f032327d62 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -105,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] ) hass.data[DOMAIN][entry.entry_id] = device_coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 499f1f3bfca..5bbcea833c2 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) CAMERA_TYPES: tuple[CameraEntityDescription, ...] = ( - CameraEntityDescription(key="activity", name="Last Activity"), + CameraEntityDescription(key="activity", name="Last activity"), CameraEntityDescription(key="avatar", name="Camera"), ) diff --git a/homeassistant/components/skybell/entity.py b/homeassistant/components/skybell/entity.py index 0e5c246a8ed..29c7167b02b 100644 --- a/homeassistant/components/skybell/entity.py +++ b/homeassistant/components/skybell/entity.py @@ -16,6 +16,7 @@ class SkybellEntity(CoordinatorEntity[SkybellDataUpdateCoordinator]): """An HA implementation for Skybell entity.""" _attr_attribution = "Data provided by Skybell.com" + _attr_has_entity_name = True def __init__( self, coordinator: SkybellDataUpdateCoordinator, description: EntityDescription @@ -23,14 +24,12 @@ class SkybellEntity(CoordinatorEntity[SkybellDataUpdateCoordinator]): """Initialize a SkyBell entity.""" super().__init__(coordinator) self.entity_description = description - if description.name != coordinator.device.name: - self._attr_name = f"{self._device.name} {description.name}" self._attr_unique_id = f"{self._device.device_id}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._device.device_id)}, manufacturer=DEFAULT_NAME, model=self._device.type, - name=self._device.name, + name=self._device.name.capitalize(), sw_version=self._device.firmware_ver, ) if self._device.mac: diff --git a/homeassistant/components/skybell/light.py b/homeassistant/components/skybell/light.py index 845be44a34b..2b3066a8827 100644 --- a/homeassistant/components/skybell/light.py +++ b/homeassistant/components/skybell/light.py @@ -23,13 +23,7 @@ async def async_setup_entry( ) -> None: """Set up Skybell switch.""" async_add_entities( - SkybellLight( - coordinator, - LightEntityDescription( - key=coordinator.device.name, - name=coordinator.device.name, - ), - ) + SkybellLight(coordinator, LightEntityDescription(key="light")) for coordinator in hass.data[DOMAIN][entry.entry_id] ) diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index eeb81e07aaf..352d29bd793 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -35,27 +35,27 @@ class SkybellSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( SkybellSensorEntityDescription( key="chime_level", - name="Chime Level", + name="Chime level", icon="mdi:bell-ring", value_fn=lambda device: device.outdoor_chime_level, ), SkybellSensorEntityDescription( key="last_button_event", - name="Last Button Event", + name="Last button event", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.latest("button").get(CONST.CREATED_AT), ), SkybellSensorEntityDescription( key="last_motion_event", - name="Last Motion Event", + name="Last motion event", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.latest("motion").get(CONST.CREATED_AT), ), SkybellSensorEntityDescription( key=CONST.ATTR_LAST_CHECK_IN, - name="Last Check in", + name="Last check in", icon="mdi:clock", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, @@ -64,7 +64,7 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), SkybellSensorEntityDescription( key="motion_threshold", - name="Motion Threshold", + name="Motion threshold", icon="mdi:walk", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -72,7 +72,7 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), SkybellSensorEntityDescription( key="video_profile", - name="Video Profile", + name="Video profile", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.video_profile, @@ -87,7 +87,7 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), SkybellSensorEntityDescription( key=CONST.ATTR_WIFI_STATUS, - name="Wifi Status", + name="Wifi status", icon="mdi:wifi-strength-3", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index d4f2817141c..529be94f1ac 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -22,15 +22,15 @@ from .entity import SkybellEntity SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="do_not_disturb", - name="Do Not Disturb", + name="Do not disturb", ), SwitchEntityDescription( key="do_not_ring", - name="Do Not Ring", + name="Do not ring", ), SwitchEntityDescription( key="motion_sensor", - name="Motion Sensor", + name="Motion sensor", ), ) diff --git a/homeassistant/components/skybell/translations/cs.json b/homeassistant/components/skybell/translations/cs.json new file mode 100644 index 00000000000..08830492748 --- /dev/null +++ b/homeassistant/components/skybell/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/pt.json b/homeassistant/components/skybell/translations/pt.json new file mode 100644 index 00000000000..8487c8869b6 --- /dev/null +++ b/homeassistant/components/skybell/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/ru.json b/homeassistant/components/skybell/translations/ru.json new file mode 100644 index 00000000000..8c30e7c8ad6 --- /dev/null +++ b/homeassistant/components/skybell/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/pt.json b/homeassistant/components/slack/translations/pt.json new file mode 100644 index 00000000000..cb9139874f2 --- /dev/null +++ b/homeassistant/components/slack/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index b98be3dcfe1..f70bed1333e 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client=gateway, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sleepiq/translations/ja.json b/homeassistant/components/sleepiq/translations/ja.json index b1e0ec1d86e..fb81857cb5f 100644 --- a/homeassistant/components/sleepiq/translations/ja.json +++ b/homeassistant/components/sleepiq/translations/ja.json @@ -13,8 +13,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "SleepIQ\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {username} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "SleepIQ\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {username} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/sleepiq/translations/pt.json b/homeassistant/components/sleepiq/translations/pt.json new file mode 100644 index 00000000000..cf42abdb666 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/__init__.py b/homeassistant/components/slimproto/__init__.py index 96932e1e81f..a96ff7ae925 100644 --- a/homeassistant/components/slimproto/__init__.py +++ b/homeassistant/components/slimproto/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = slimserver # initialize platform(s) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # setup event listeners async def on_hass_stop(event: Event) -> None: diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index 2f85aa4b9df..bcd538f57e8 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -74,6 +74,7 @@ async def async_setup_entry( class SlimProtoPlayer(MediaPlayerEntity): """Representation of MediaPlayerEntity from SlimProto Player.""" + _attr_has_entity_name = True _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -139,7 +140,6 @@ class SlimProtoPlayer(MediaPlayerEntity): @callback def update_attributes(self) -> None: """Handle player updates.""" - self._attr_name = self.player.name self._attr_volume_level = self.player.volume_level / 100 self._attr_media_position = self.player.elapsed_seconds self._attr_media_position_updated_at = utcnow() diff --git a/homeassistant/components/slimproto/translations/ja.json b/homeassistant/components/slimproto/translations/ja.json index 4b8d0691b68..cf3ac93acad 100644 --- a/homeassistant/components/slimproto/translations/ja.json +++ b/homeassistant/components/slimproto/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" } } } \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/pt.json b/homeassistant/components/slimproto/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/slimproto/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 556c2a16b6c..13f402b53c3 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -120,7 +120,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PYSMA_DEVICE_INFO: device_info, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sma/translations/pt.json b/homeassistant/components/sma/translations/pt.json new file mode 100644 index 00000000000..a37e6656da7 --- /dev/null +++ b/homeassistant/components/sma/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 43c7aca9a34..c7edd46c7e2 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -105,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = SmappeeBase(hass, smappee) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 712cbd0951d..f1e0e5012ad 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_configured_local_device": "A helyi eszk\u00f6z\u00f6k m\u00e1r konfigur\u00e1lva vannak. K\u00e9rj\u00fck, el\u0151sz\u00f6r t\u00e1vol\u00edtsa el ezeket, miel\u0151tt konfigur\u00e1lja a felh\u0151alap\u00fa eszk\u00f6zt.", + "already_configured_local_device": "A helyi eszk\u00f6z\u00f6k m\u00e1r konfigur\u00e1lva vannak. K\u00e9rem, el\u0151sz\u00f6r t\u00e1vol\u00edtsa el ezeket a rendszerb\u0151l, miel\u0151tt bekonfigur\u00e1lja \u0151ket felh\u0151 alapon.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_mdns": "Nem t\u00e1mogatott eszk\u00f6z a Smappee integr\u00e1ci\u00f3hoz.", diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json index 9dff009e3dd..aabbfd52564 100644 --- a/homeassistant/components/smappee/translations/ja.json +++ b/homeassistant/components/smappee/translations/ja.json @@ -5,7 +5,7 @@ "already_configured_local_device": "\u30ed\u30fc\u30ab\u30eb\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30af\u30e9\u30a6\u30c9\u30c7\u30d0\u30a4\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u524d\u306b\u3001\u307e\u305a\u305d\u308c\u3089\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_mdns": "Smappee\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3002", + "invalid_mdns": "Smappee\u7d71\u5408\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" }, @@ -21,7 +21,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30db\u30b9\u30c8\u3092\u5165\u529b\u3057\u3066\u3001Smappee local\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u59cb\u3057\u307e\u3059" + "description": "\u30db\u30b9\u30c8\u3092\u5165\u529b\u3057\u3066\u3001Smappee local\u7d71\u5408\u3092\u958b\u59cb\u3057\u307e\u3059" }, "pick_implementation": { "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index cd83a1e51c1..0fcbedca874 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: asyncio.create_task(coordinator.async_refresh()) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 3f10758076f..c45702773b2 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -183,7 +183,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 87d20b4533f..db13b5e6b37 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -106,9 +106,7 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: Capability.air_conditioner_mode, Capability.demand_response_load_control, Capability.air_conditioner_fan_mode, - Capability.relative_humidity_measurement, Capability.switch, - Capability.temperature_measurement, Capability.thermostat, Capability.thermostat_cooling_setpoint, Capability.thermostat_fan_mode, diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index fbd63d41373..adf0426e9a2 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -3,6 +3,7 @@ import asyncio import functools import logging import secrets +from typing import Any from urllib.parse import urlparse from uuid import uuid4 @@ -211,8 +212,8 @@ async def setup_smartapp_endpoint(hass: HomeAssistant): return # Get/create config to store a unique id for this hass instance. - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - if not (config := await store.async_load()) or not isinstance(config, dict): + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + if not (config := await store.async_load()): # Create config config = { CONF_INSTANCE_ID: str(uuid4()), @@ -283,7 +284,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistant): if cloudhook_url and cloud.async_is_logged_in(hass): await cloud.async_delete_cloudhook(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) # Remove cloudhook from storage - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) await store.async_save( { CONF_INSTANCE_ID: hass.data[DOMAIN][CONF_INSTANCE_ID], diff --git a/homeassistant/components/smartthings/translations/de.json b/homeassistant/components/smartthings/translations/de.json index 6cd7157b702..b6a97013784 100644 --- a/homeassistant/components/smartthings/translations/de.json +++ b/homeassistant/components/smartthings/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant ist nicht richtig konfiguriert, um Updates von SmartThings zu erhalten. Die Webhook-URL ist ung\u00fcltig: \n > {webhook_url} \n\nBitte aktualisiere deine Konfiguration gem\u00e4\u00df den [Anweisungen] ({component_url}), starte den Home Assistant neu und versuche es erneut.", + "invalid_webhook_url": "Home Assistant ist nicht richtig konfiguriert, um Updates von SmartThings zu erhalten. Die Webhook-URL ist ung\u00fcltig: \n \u2192 {webhook_url} \n\nBitte aktualisiere deine Konfiguration gem\u00e4\u00df den [Anweisungen] ({component_url}), starte den Home Assistant neu und versuche es erneut.", "no_available_locations": "In Home Assistant sind keine SmartThings-Standorte zum Einrichten verf\u00fcgbar." }, "error": { @@ -30,7 +30,7 @@ "title": "Standort ausw\u00e4hlen" }, "user": { - "description": "SmartThings wird so konfiguriert, dass Push-Updates an Home Assistant gesendet werden an die URL: \n > {webhook_url} \n\nWenn dies nicht korrekt ist, aktualisiere bitte deine Konfiguration, starte Home Assistant neu und versuche es erneut.", + "description": "SmartThings wird so konfiguriert, dass Push-Updates an Home Assistant gesendet werden an die URL: \n \u2192 {webhook_url} \n\nWenn dies nicht korrekt ist, aktualisiere bitte deine Konfiguration, starte Home Assistant neu und versuche es erneut.", "title": "R\u00fcckruf-URL best\u00e4tigen" } } diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index 90ea748ae33..47698ef528c 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant nincs megfelel\u0151en konfigur\u00e1lva a SmartThings friss\u00edt\u00e9seinek fogad\u00e1s\u00e1ra. A webhook URL \u00e9rv\u00e9nytelen:\n > {webhook_url} \n\nK\u00e9rj\u00fck, friss\u00edtse konfigur\u00e1ci\u00f3j\u00e1t az [utas\u00edt\u00e1sok]({component_url}) szerint, ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "invalid_webhook_url": "Home Assistant nincs megfelel\u0151en konfigur\u00e1lva a SmartThings friss\u00edt\u00e9seinek fogad\u00e1s\u00e1ra. A webhook URL \u00e9rv\u00e9nytelen:\n > {webhook_url} \n\nK\u00e9rem, friss\u00edtse konfigur\u00e1ci\u00f3j\u00e1t az [utas\u00edt\u00e1sok]({component_url}) szerint, ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", "no_available_locations": "Nincsenek be\u00e1ll\u00edthat\u00f3 SmartThings helyek a Home Assistant alkalmaz\u00e1sban." }, "error": { @@ -9,7 +9,7 @@ "token_forbidden": "A token nem rendelkezik a sz\u00fcks\u00e9ges OAuth-tartom\u00e1nyokkal.", "token_invalid_format": "A tokennek UID / GUID form\u00e1tumban kell lennie", "token_unauthorized": "A token \u00e9rv\u00e9nytelen vagy m\u00e1r nem enged\u00e9lyezett.", - "webhook_error": "SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a webhook URL-t. K\u00e9rj\u00fck, ellen\u0151rizze, hogy a webhook URL el\u00e9rhet\u0151-e az internet fel\u0151l, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra." + "webhook_error": "SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a webhook URL-t. K\u00e9rem, ellen\u0151rizze, hogy a webhook URL el\u00e9rhet\u0151-e az internet fel\u0151l, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra." }, "step": { "authorize": { @@ -19,14 +19,14 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban.", + "description": "K\u00e9rem, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban.", "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent" }, "select_location": { "data": { "location_id": "Elhelyezked\u00e9s" }, - "description": "K\u00e9rj\u00fck, v\u00e1lassza ki azt a SmartThings helyet, amelyet hozz\u00e1 szeretne adni a Home Assistant szolg\u00e1ltat\u00e1shoz. Ezut\u00e1n \u00faj ablakot nyitunk, \u00e9s megk\u00e9rj\u00fck, hogy jelentkezzen be, \u00e9s enged\u00e9lyezze a Home Assistant integr\u00e1ci\u00f3j\u00e1nak telep\u00edt\u00e9s\u00e9t a kiv\u00e1lasztott helyre.", + "description": "K\u00e9rem, v\u00e1lassza ki azt a SmartThings helyet, amelyet hozz\u00e1 szeretne adni a Home Assistant szolg\u00e1ltat\u00e1shoz. Ezut\u00e1n az \u00faj ablakban jelentkezzen be, \u00e9s enged\u00e9lyezze a Home Assistant integr\u00e1ci\u00f3j\u00e1nak telep\u00edt\u00e9s\u00e9t a kiv\u00e1lasztott helyre.", "title": "Hely kiv\u00e1laszt\u00e1sa" }, "user": { diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index e522744cd4d..94d1aba2606 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -19,14 +19,14 @@ "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, - "description": "[\u624b\u9806]({component_url})\u3054\u3068\u306b\u4f5c\u6210\u3055\u308c\u305f\u3001SmartThings[\u500b\u4eba\u7528\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3]({token_url})\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u3053\u308c\u306f\u3001SmartThings account\u5185\u306bHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f5c\u6210\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", + "description": "[\u624b\u9806]({component_url})\u3054\u3068\u306b\u4f5c\u6210\u3055\u308c\u305f\u3001SmartThings[\u500b\u4eba\u7528\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3]({token_url})\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u3053\u308c\u306f\u3001SmartThings account\u5185\u306bHome Assistant\u7d71\u5408\u3092\u4f5c\u6210\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", "title": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "select_location": { "data": { "location_id": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" }, - "description": "Home Assistant\u306b\u8ffd\u52a0\u3057\u305f\u3044SmartThings\u306e\u5834\u6240\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3059\u308b\u3068\u3001\u65b0\u3057\u3044\u30a6\u30a3\u30f3\u30c9\u30a6\u304c\u958b\u304f\u306e\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u9078\u629e\u3057\u305f\u5834\u6240\u3078\u306eHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u627f\u8a8d\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002", + "description": "Home Assistant\u306b\u8ffd\u52a0\u3057\u305f\u3044SmartThings\u306e\u5834\u6240\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3059\u308b\u3068\u3001\u65b0\u3057\u3044\u30a6\u30a3\u30f3\u30c9\u30a6\u304c\u958b\u304f\u306e\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u9078\u629e\u3057\u305f\u5834\u6240\u3078\u306eHome Assistant\u7d71\u5408\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u627f\u8a8d\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002", "title": "\u5834\u6240\u3092\u9078\u629e" }, "user": { diff --git a/homeassistant/components/smartthings/translations/pt.json b/homeassistant/components/smartthings/translations/pt.json index 9f2ed5a4b90..a6297502b0e 100644 --- a/homeassistant/components/smartthings/translations/pt.json +++ b/homeassistant/components/smartthings/translations/pt.json @@ -28,7 +28,7 @@ "title": "Selecionar Localiza\u00e7\u00e3o" }, "user": { - "description": "Por favor, insira um SmartThings [Personal Access Token]({token_url} ) que foi criado de acordo com as [instru\u00e7\u00f5es]({component_url}).", + "description": "SmartThings will be configured to send push updates to Home Assistant at:\n> {webhook_url}\n\nIf this is not correct, please update your configuration, restart Home Assistant, and try again.", "title": "Insira o Token de acesso pessoal" } } diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index f6e36a847e5..f98dbac86a1 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await controller.async_setup_entry(entry): return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smarttub/translations/ja.json b/homeassistant/components/smarttub/translations/ja.json index 44d753fa22e..1488ddf9e16 100644 --- a/homeassistant/components/smarttub/translations/ja.json +++ b/homeassistant/components/smarttub/translations/ja.json @@ -9,8 +9,8 @@ }, "step": { "reauth_confirm": { - "description": "SmartTub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "SmartTub\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/smarttub/translations/pt.json b/homeassistant/components/smarttub/translations/pt.json index 5d4e3e1faed..c6325f3dba3 100644 --- a/homeassistant/components/smarttub/translations/pt.json +++ b/homeassistant/components/smarttub/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Conta j\u00e1 configurada", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 98c8de87032..5b3f60f4b08 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -20,7 +20,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unique_id = f"{entry.data[CONF_LOCATION][CONF_LATITUDE]}-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}" hass.config_entries.async_update_entry(entry, unique_id=unique_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smhi/translations/pt.json b/homeassistant/components/smhi/translations/pt.json index d5cd5e83a13..acdfb6606bd 100644 --- a/homeassistant/components/smhi/translations/pt.json +++ b/homeassistant/components/smhi/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia" }, diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index d7df54957c0..f8afcb7b59a 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -126,6 +126,8 @@ class SmhiWeather(WeatherEntity): _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND _attr_native_pressure_unit = PRESSURE_HPA + _attr_has_entity_name = True + def __init__( self, name: str, @@ -134,8 +136,6 @@ class SmhiWeather(WeatherEntity): session: aiohttp.ClientSession, ) -> None: """Initialize the SMHI weather entity.""" - - self._attr_name = name self._attr_unique_id = f"{latitude}, {longitude}" self._forecasts: list[SmhiForecast] | None = None self._fail_count = 0 diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 0b63a3d0366..83d4bbc31f3 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: GATEWAY: gateway, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sms/translations/ja.json b/homeassistant/components/sms/translations/ja.json index ddfb644d90f..88780e7aef0 100644 --- a/homeassistant/components/sms/translations/ja.json +++ b/homeassistant/components/sms/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/sochain/__init__.py b/homeassistant/components/sochain/__init__.py deleted file mode 100644 index 31a000d3947..00000000000 --- a/homeassistant/components/sochain/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sochain component.""" diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json deleted file mode 100644 index 5a568340197..00000000000 --- a/homeassistant/components/sochain/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "disabled": "Integration library depends on async_timeout==3.x.x", - "domain": "sochain", - "name": "SoChain", - "documentation": "https://www.home-assistant.io/integrations/sochain", - "requirements": ["python-sochain-api==0.0.2"], - "codeowners": [], - "iot_class": "cloud_polling", - "loggers": ["pysochain"] -} diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py deleted file mode 100644 index 157d94b8706..00000000000 --- a/homeassistant/components/sochain/sensor.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Support for watching multiple cryptocurrencies.""" -# pylint: disable=import-error -from __future__ import annotations - -from datetime import timedelta - -from pysochain import ChainSo -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -ATTRIBUTION = "Data provided by chain.so" - -CONF_NETWORK = "network" - -DEFAULT_NAME = "Crypto Balance" - -SCAN_INTERVAL = timedelta(minutes=5) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Required(CONF_NETWORK): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the sochain sensors.""" - - address = config[CONF_ADDRESS] - network = config[CONF_NETWORK] - name = config[CONF_NAME] - - session = async_get_clientsession(hass) - chainso = ChainSo(network, address, hass.loop, session) - - async_add_entities([SochainSensor(name, network.upper(), chainso)], True) - - -class SochainSensor(SensorEntity): - """Representation of a Sochain sensor.""" - - def __init__(self, name, unit_of_measurement, chainso): - """Initialize the sensor.""" - self._name = name - self._unit_of_measurement = unit_of_measurement - self.chainso = chainso - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return ( - self.chainso.data.get("confirmed_balance") - if self.chainso is not None - else None - ) - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - async def async_update(self): - """Get the latest state of the sensor.""" - await self.chainso.async_get_data() diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 148e684589f..dca129c7a70 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_API_CLIENT: api} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 58e5577ccfa..8bdf6a4b4aa 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -23,7 +23,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._errors = {} + self._errors: dict[str, str] = {} @callback def _async_current_site_ids(self) -> set[str]: diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 916a01de9e9..fd989fc8a2a 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -75,6 +75,7 @@ SENSOR_TYPES = [ ), SolarEdgeSensorEntityDescription( key="site_details", + json_key="status", name="Site details", entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index fe8f2f86a8e..7d3e949fc10 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import abstractmethod from datetime import date, datetime, timedelta +from typing import Any from solaredge import Solaredge from stringcase import snakecase @@ -23,16 +24,17 @@ from .const import ( class SolarEdgeDataService: """Get and update the latest data.""" + coordinator: DataUpdateCoordinator + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: """Initialize the data object.""" self.api = api self.site_id = site_id - self.data = {} - self.attributes = {} + self.data: dict[str, Any] = {} + self.attributes: dict[str, Any] = {} self.hass = hass - self.coordinator = None @callback def async_setup(self) -> None: @@ -105,12 +107,6 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): class SolarEdgeDetailsDataService(SolarEdgeDataService): """Get and update the latest details data.""" - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: - """Initialize the details data service.""" - super().__init__(hass, api, site_id) - - self.data = None - @property def update_interval(self) -> timedelta: """Update interval.""" @@ -125,7 +121,7 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): except KeyError as ex: raise UpdateFailed("Missing details data, skipping update") from ex - self.data = None + self.data = {} self.attributes = {} for key, value in details.items(): @@ -143,9 +139,13 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): ]: self.attributes[key] = value elif key == "status": - self.data = value + self.data["status"] = value - LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes) + LOGGER.debug( + "Updated SolarEdge details: %s, %s", + self.data.get("status"), + self.attributes, + ) class SolarEdgeInventoryDataService(SolarEdgeDataService): diff --git a/homeassistant/components/solaredge/models.py b/homeassistant/components/solaredge/models.py index ce24d854aac..57efb88023c 100644 --- a/homeassistant/components/solaredge/models.py +++ b/homeassistant/components/solaredge/models.py @@ -7,7 +7,14 @@ from homeassistant.components.sensor import SensorEntityDescription @dataclass -class SolarEdgeSensorEntityDescription(SensorEntityDescription): - """Sensor entity description for SolarEdge.""" +class SolarEdgeSensorEntityRequiredKeyMixin: + """Sensor entity description with json_key for SolarEdge.""" - json_key: str | None = None + json_key: str + + +@dataclass +class SolarEdgeSensorEntityDescription( + SensorEntityDescription, SolarEdgeSensorEntityRequiredKeyMixin +): + """Sensor entity description for SolarEdge.""" diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index a769a043442..a27c180e0b9 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -101,7 +101,7 @@ class SolarEdgeSensorFactory: def create_sensor( self, sensor_type: SolarEdgeSensorEntityDescription - ) -> SolarEdgeSensorEntityDescription: + ) -> SolarEdgeSensorEntity: """Create and return a sensor based on the sensor_key.""" sensor_class, service = self.services[sensor_type.key] @@ -155,7 +155,7 @@ class SolarEdgeDetailsSensor(SolarEdgeSensorEntity): @property def native_value(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data + return self.data_service.data.get(self.entity_description.json_key) @property def unique_id(self) -> str | None: @@ -169,7 +169,7 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API inventory sensor.""" @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self.data_service.attributes.get(self.entity_description.json_key) @@ -182,14 +182,19 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API power flow sensor.""" - def __init__(self, platform_name, sensor_type, data_service): + def __init__( + self, + platform_name: str, + sensor_type: SolarEdgeSensorEntityDescription, + data_service: SolarEdgeEnergyDetailsService, + ) -> None: """Initialize the power flow sensor.""" super().__init__(platform_name, sensor_type, data_service) self._attr_native_unit_of_measurement = data_service.unit @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self.data_service.attributes.get(self.entity_description.json_key) @@ -208,7 +213,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): self, platform_name: str, description: SolarEdgeSensorEntityDescription, - data_service: SolarEdgeDataService, + data_service: SolarEdgePowerFlowDataService, ) -> None: """Initialize the power flow sensor.""" super().__init__(platform_name, description, data_service) @@ -216,7 +221,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): self._attr_native_unit_of_measurement = data_service.unit @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self.data_service.attributes.get(self.entity_description.json_key) diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index 0ebcc3782c9..22c0e3d87d7 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = SolarlogData(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/solax/__init__.py b/homeassistant/components/solax/__init__.py index 3915f414d0e..6ede5b5df02 100644 --- a/homeassistant/components/solax/__init__.py +++ b/homeassistant/components/solax/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/solax/translations/cs.json b/homeassistant/components/solax/translations/cs.json index 7940c6378fe..2bf3fbfa3fb 100644 --- a/homeassistant/components/solax/translations/cs.json +++ b/homeassistant/components/solax/translations/cs.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "ip_address": "IP adresa", "port": "Port" } } diff --git a/homeassistant/components/solax/translations/pt.json b/homeassistant/components/solax/translations/pt.json new file mode 100644 index 00000000000..916bd3daced --- /dev/null +++ b/homeassistant/components/solax/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 034a6a0b782..1b3478b2897 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) hass.data[DOMAIN][DEVICES] = devices["shades"] - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json index 89194a7b204..3cd6dff8c2e 100644 --- a/homeassistant/components/soma/translations/hu.json +++ b/homeassistant/components/soma/translations/hu.json @@ -4,7 +4,7 @@ "already_setup": "Csak egy Soma-fi\u00f3k konfigur\u00e1lhat\u00f3.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "connection_error": "Nem siker\u00fclt csatlakozni.", - "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A Soma komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "result_error": "A SOMA Connect hiba\u00e1llapottal v\u00e1laszolt." }, "create_entry": { @@ -16,7 +16,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", + "description": "K\u00e9rem, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", "title": "SOMA Connect" } } diff --git a/homeassistant/components/soma/translations/ja.json b/homeassistant/components/soma/translations/ja.json index 026499458d8..d3415c1d197 100644 --- a/homeassistant/components/soma/translations/ja.json +++ b/homeassistant/components/soma/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "missing_configuration": "SOMA Connect\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/soma/translations/pt.json b/homeassistant/components/soma/translations/pt.json index f681da4210f..9199c825f4a 100644 --- a/homeassistant/components/soma/translations/pt.json +++ b/homeassistant/components/soma/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_setup": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index a7fb40eafd4..8ac6c4672fd 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/somfy_mylink/translations/bg.json b/homeassistant/components/somfy_mylink/translations/bg.json index ca0ed419f99..5ee98a5d46c 100644 --- a/homeassistant/components/somfy_mylink/translations/bg.json +++ b/homeassistant/components/somfy_mylink/translations/bg.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index 49f819659b4..97b8e990af7 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -16,7 +16,7 @@ "port": "\u30dd\u30fc\u30c8", "system_id": "\u30b7\u30b9\u30c6\u30e0ID" }, - "description": "\u30b7\u30b9\u30c6\u30e0ID \u306f\u3001\u30af\u30e9\u30a6\u30c9\u4ee5\u5916\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u9078\u629e\u3059\u308b\u3053\u3068\u306b\u3088\u308a\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e MyLink \u30a2\u30d7\u30ea\u3067\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002" + "description": "\u30b7\u30b9\u30c6\u30e0ID \u306f\u3001\u30af\u30e9\u30a6\u30c9\u4ee5\u5916\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u9078\u629e\u3059\u308b\u3053\u3068\u306b\u3088\u308a\u3001\u7d71\u5408\u306e MyLink \u30a2\u30d7\u30ea\u3067\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/somfy_mylink/translations/pt.json b/homeassistant/components/somfy_mylink/translations/pt.json new file mode 100644 index 00000000000..fe98366e28e --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "" + }, + "options": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 9934cc8f481..4447425f42a 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_SYSTEM_STATUS: system_status, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 6b34b077888..6b9b45b75ec 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.6.0"], + "requirements": ["aiopyarr==22.7.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index cbe230bbc94..d37c4915481 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -12,8 +12,8 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u30db\u30b9\u30c8\u3055\u308c\u3066\u3044\u308bSonarr API\u3067\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {host}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Sonarr\u7d71\u5408\u306f\u3001\u30db\u30b9\u30c8\u3055\u308c\u3066\u3044\u308bSonarr API\u3067\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {host}", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index 64e71af3e6a..8ab1cb18bdd 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -38,7 +38,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up songpal media player.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index e3f65d754dd..d94b49e52f2 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -143,7 +143,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = SonosDiscoveryManager( hass, entry, data, hosts ) - hass.async_create_task(manager.setup_platforms_and_discovery()) + await manager.setup_platforms_and_discovery() return True @@ -201,7 +201,11 @@ class SonosDiscoveryManager: speaker.activity_stats.log_report() speaker.event_stats.log_report() if zgs := next( - speaker.soco.zone_group_state for speaker in self.data.discovered.values() + ( + speaker.soco.zone_group_state + for speaker in self.data.discovered.values() + ), + None, ): _LOGGER.debug( "ZoneGroupState stats: (%s/%s) processed", @@ -377,12 +381,7 @@ class SonosDiscoveryManager: async def setup_platforms_and_discovery(self): """Set up platforms and discovery.""" - await asyncio.gather( - *( - self.hass.config_entries.async_forward_entry_setup(self.entry, platform) - for platform in PLATFORMS - ) - ) + await self.hass.config_entries.async_forward_entry_setups(self.entry, PLATFORMS) self.entry.async_on_unload( self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 4eaa75f92ae..e890c1c64a8 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -58,12 +58,12 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING + _attr_name = "Power" def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the power entity binary sensor.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-power" - self._attr_name = f"{self.speaker.zone_name} Power" async def _async_fallback_poll(self) -> None: """Poll the device for the current state.""" @@ -92,12 +92,12 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:microphone" + _attr_name = "Microphone" def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the microphone binary sensor entity.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-microphone" - self._attr_name = f"{self.speaker.zone_name} Microphone" async def _async_fallback_poll(self) -> None: """Handle polling when subscription fails.""" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index ba1f72cd56b..0955fb0e82d 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -26,6 +26,7 @@ class SonosEntity(Entity): """Representation of a Sonos entity.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, speaker: SonosSpeaker) -> None: """Initialize a SonosEntity.""" diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py index 0d76feae461..51d7e9cec8c 100644 --- a/homeassistant/components/sonos/household_coordinator.py +++ b/homeassistant/components/sonos/household_coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine import logging +from typing import Any from soco import SoCo @@ -35,7 +36,7 @@ class SonosHouseholdCoordinator: async def _async_setup(self) -> None: """Finish setup in async context.""" self.cache_update_lock = asyncio.Lock() - self.async_poll = Debouncer( + self.async_poll = Debouncer[Coroutine[Any, Any, None]]( self.hass, _LOGGER, cooldown=3, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 470386c341d..1f57cafbf09 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -82,8 +82,6 @@ SONOS_TO_REPEAT = {meaning: mode for mode, meaning in REPEAT_TO_SONOS.items()} UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"] -SERVICE_JOIN = "join" -SERVICE_UNJOIN = "unjoin" SERVICE_SNAPSHOT = "snapshot" SERVICE_RESTORE = "restore" SERVICE_SET_TIMER = "set_sleep_timer" @@ -130,24 +128,7 @@ async def async_setup_entry( assert isinstance(entity, SonosMediaPlayerEntity) speakers.append(entity.speaker) - if service_call.service == SERVICE_JOIN: - _LOGGER.warning( - "Service 'sonos.join' is deprecated and will be removed in 2022.8, please use 'media_player.join'" - ) - master = platform.entities.get(service_call.data[ATTR_MASTER]) - if master: - await SonosSpeaker.join_multi(hass, master.speaker, speakers) # type: ignore[arg-type] - else: - _LOGGER.error( - "Invalid master specified for join service: %s", - service_call.data[ATTR_MASTER], - ) - elif service_call.service == SERVICE_UNJOIN: - _LOGGER.warning( - "Service 'sonos.unjoin' is deprecated and will be removed in 2022.8, please use 'media_player.unjoin'" - ) - await SonosSpeaker.unjoin_multi(hass, speakers) # type: ignore[arg-type] - elif service_call.service == SERVICE_SNAPSHOT: + if service_call.service == SERVICE_SNAPSHOT: await SonosSpeaker.snapshot_multi( hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) @@ -160,20 +141,6 @@ async def async_setup_entry( async_dispatcher_connect(hass, SONOS_CREATE_MEDIA_PLAYER, async_create_entities) ) - hass.services.async_register( - SONOS_DOMAIN, - SERVICE_JOIN, - async_service_handle, - cv.make_entity_service_schema({vol.Required(ATTR_MASTER): cv.entity_id}), - ) - - hass.services.async_register( - SONOS_DOMAIN, - SERVICE_UNJOIN, - async_service_handle, - cv.make_entity_service_schema({}), - ) - join_unjoin_schema = cv.make_entity_service_schema( {vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean} ) @@ -249,7 +216,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Initialize the media player entity.""" super().__init__(speaker) self._attr_unique_id = self.soco.uid - self._attr_name = self.speaker.zone_name async def async_added_to_hass(self) -> None: """Handle common setup when added to hass.""" diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 3b034423471..ccbcbc3c339 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -72,8 +72,7 @@ class SonosLevelEntity(SonosEntity, NumberEntity): """Initialize the level entity.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-{level_type}" - name_suffix = level_type.replace("_", " ").title() - self._attr_name = f"{self.speaker.zone_name} {name_suffix}" + self._attr_name = level_type.replace("_", " ").capitalize() self.level_type = level_type self._attr_native_min_value, self._attr_native_max_value = valid_range diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 380d1a3b9b6..8477e523a40 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -80,13 +80,13 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_name = "Battery" _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the battery sensor.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-battery" - self._attr_name = f"{self.speaker.zone_name} Battery" async def _async_fallback_poll(self) -> None: """Poll the device for the current state.""" @@ -108,13 +108,13 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:import" + _attr_name = "Audio input format" _attr_should_poll = True def __init__(self, speaker: SonosSpeaker, audio_format: str) -> None: """Initialize the audio input format sensor.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-audio-format" - self._attr_name = f"{self.speaker.zone_name} Audio Input Format" self._attr_native_value = audio_format def poll_state(self) -> None: @@ -137,7 +137,7 @@ class SonosFavoritesEntity(SensorEntity): _attr_entity_registry_enabled_default = False _attr_icon = "mdi:star" - _attr_name = "Sonos Favorites" + _attr_name = "Sonos favorites" _attr_native_unit_of_measurement = "items" _attr_should_poll = False diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index a172b45ecd9..9d61c20f7cb 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -1,32 +1,3 @@ -join: - name: Join group - description: Group player together. - fields: - master: - name: Master - description: Entity ID of the player that should become the coordinator of the group. - required: true - selector: - entity: - integration: sonos - domain: media_player - entity_id: - name: Entity - description: Name of entity that will join the master. - required: true - selector: - entity: - integration: sonos - domain: media_player - -unjoin: - name: Unjoin group - description: Unjoin the player from a group. - target: - entity: - integration: sonos - domain: media_player - snapshot: name: Snapshot description: Take a snapshot of the media player. diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 53911d85d3e..a348b40cb0f 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -69,13 +69,13 @@ POLL_REQUIRED = ( FRIENDLY_NAMES = { ATTR_CROSSFADE: "Crossfade", ATTR_LOUDNESS: "Loudness", - ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround Music Full Volume", - ATTR_NIGHT_SOUND: "Night Sound", - ATTR_SPEECH_ENHANCEMENT: "Speech Enhancement", - ATTR_STATUS_LIGHT: "Status Light", - ATTR_SUB_ENABLED: "Subwoofer Enabled", - ATTR_SURROUND_ENABLED: "Surround Enabled", - ATTR_TOUCH_CONTROLS: "Touch Controls", + ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround music full volume", + ATTR_NIGHT_SOUND: "Night sound", + ATTR_SPEECH_ENHANCEMENT: "Speech enhancement", + ATTR_STATUS_LIGHT: "Status light", + ATTR_SUB_ENABLED: "Subwoofer enabled", + ATTR_SURROUND_ENABLED: "Surround enabled", + ATTR_TOUCH_CONTROLS: "Touch controls", } FEATURE_ICONS = { @@ -160,7 +160,7 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): self.feature_type = feature_type self.needs_coordinator = feature_type in COORDINATOR_FEATURES self._attr_entity_category = EntityCategory.CONFIG - self._attr_name = f"{speaker.zone_name} {FRIENDLY_NAMES[feature_type]}" + self._attr_name = FRIENDLY_NAMES[feature_type] self._attr_unique_id = f"{speaker.soco.uid}-{feature_type}" self._attr_icon = FEATURE_ICONS.get(feature_type) @@ -240,11 +240,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): @property def name(self) -> str: """Return the name of the sensor.""" - return "{} {} Alarm {}".format( - self.speaker.zone_name, - self.alarm.recurrence.title(), - str(self.alarm.start_time)[0:5], - ) + return f"{self.alarm.recurrence.capitalize()} alarm {str(self.alarm.start_time)[:5]}" async def _async_fallback_poll(self) -> None: """Call the central alarm polling method.""" diff --git a/homeassistant/components/sonos/translations/ja.json b/homeassistant/components/sonos/translations/ja.json index 7aa5823c7c6..d4b696cbb48 100644 --- a/homeassistant/components/sonos/translations/ja.json +++ b/homeassistant/components/sonos/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "not_sonos_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Sonos\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/translations/pt.json b/homeassistant/components/sonos/translations/pt.json index 51fbd16a20d..5b0b34a8eb7 100644 --- a/homeassistant/components/sonos/translations/pt.json +++ b/homeassistant/components/sonos/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo Sonos encontrado na rede.", + "no_devices_found": "Nenhum dispositivo encontrado na rede", "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Sonos \u00e9 necess\u00e1ria." }, "step": { diff --git a/homeassistant/components/soundtouch/__init__.py b/homeassistant/components/soundtouch/__init__.py index 6cd3c88fefc..69e0eef687e 100644 --- a/homeassistant/components/soundtouch/__init__.py +++ b/homeassistant/components/soundtouch/__init__.py @@ -1 +1,142 @@ """The soundtouch component.""" +import logging + +from libsoundtouch import soundtouch_device +from libsoundtouch.device import SoundTouchDevice +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, Platform +from homeassistant.core import HomeAssistant, ServiceCall +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import ( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + SERVICE_CREATE_ZONE, + SERVICE_PLAY_EVERYWHERE, + SERVICE_REMOVE_ZONE_SLAVE, +) + +_LOGGER = logging.getLogger(__name__) + +SERVICE_PLAY_EVERYWHERE_SCHEMA = vol.Schema({vol.Required("master"): cv.entity_id}) +SERVICE_CREATE_ZONE_SCHEMA = vol.Schema( + { + vol.Required("master"): cv.entity_id, + vol.Required("slaves"): cv.entity_ids, + } +) +SERVICE_ADD_ZONE_SCHEMA = vol.Schema( + { + vol.Required("master"): cv.entity_id, + vol.Required("slaves"): cv.entity_ids, + } +) +SERVICE_REMOVE_ZONE_SCHEMA = vol.Schema( + { + vol.Required("master"): cv.entity_id, + vol.Required("slaves"): cv.entity_ids, + } +) + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +class SoundTouchData: + """SoundTouch data stored in the Home Assistant data object.""" + + def __init__(self, device: SoundTouchDevice) -> None: + """Initialize the SoundTouch data object for a device.""" + self.device = device + self.media_player = None + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Bose SoundTouch component.""" + + async def service_handle(service: ServiceCall) -> None: + """Handle the applying of a service.""" + master_id = service.data.get("master") + slaves_ids = service.data.get("slaves") + slaves = [] + if slaves_ids: + slaves = [ + data.media_player + for data in hass.data[DOMAIN].values() + if data.media_player.entity_id in slaves_ids + ] + + master = next( + iter( + [ + data.media_player + for data in hass.data[DOMAIN].values() + if data.media_player.entity_id == master_id + ] + ), + None, + ) + + if master is None: + _LOGGER.warning("Unable to find master with entity_id: %s", str(master_id)) + return + + if service.service == SERVICE_PLAY_EVERYWHERE: + slaves = [ + data.media_player + for data in hass.data[DOMAIN].values() + if data.media_player.entity_id != master_id + ] + await hass.async_add_executor_job(master.create_zone, slaves) + elif service.service == SERVICE_CREATE_ZONE: + await hass.async_add_executor_job(master.create_zone, slaves) + elif service.service == SERVICE_REMOVE_ZONE_SLAVE: + await hass.async_add_executor_job(master.remove_zone_slave, slaves) + elif service.service == SERVICE_ADD_ZONE_SLAVE: + await hass.async_add_executor_job(master.add_zone_slave, slaves) + + hass.services.async_register( + DOMAIN, + SERVICE_PLAY_EVERYWHERE, + service_handle, + schema=SERVICE_PLAY_EVERYWHERE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_CREATE_ZONE, + service_handle, + schema=SERVICE_CREATE_ZONE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_REMOVE_ZONE_SLAVE, + service_handle, + schema=SERVICE_REMOVE_ZONE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + service_handle, + schema=SERVICE_ADD_ZONE_SCHEMA, + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Bose SoundTouch from a config entry.""" + device = await hass.async_add_executor_job(soundtouch_device, entry.data[CONF_HOST]) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/soundtouch/config_flow.py b/homeassistant/components/soundtouch/config_flow.py new file mode 100644 index 00000000000..47b10912436 --- /dev/null +++ b/homeassistant/components/soundtouch/config_flow.py @@ -0,0 +1,104 @@ +"""Config flow for Bose SoundTouch integration.""" +import logging + +from libsoundtouch import soundtouch_device +from requests import RequestException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class SoundtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Bose SoundTouch.""" + + VERSION = 1 + + def __init__(self): + """Initialize a new SoundTouch config flow.""" + self.host = None + self.name = None + + async def async_step_import(self, import_data): + """Handle a flow initiated by configuration file.""" + self.host = import_data[CONF_HOST] + + try: + await self._async_get_device_id() + except RequestException: + return self.async_abort(reason="cannot_connect") + + return await self._async_create_soundtouch_entry() + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is not None: + self.host = user_input[CONF_HOST] + + try: + await self._async_get_device_id(raise_on_progress=False) + except RequestException: + errors["base"] = "cannot_connect" + else: + return await self._async_create_soundtouch_entry() + + return self.async_show_form( + step_id="user", + last_step=True, + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + } + ), + errors=errors, + ) + + async def async_step_zeroconf(self, discovery_info): + """Handle a flow initiated by a zeroconf discovery.""" + self.host = discovery_info.host + + try: + await self._async_get_device_id() + except RequestException: + return self.async_abort(reason="cannot_connect") + + self.context["title_placeholders"] = {"name": self.name} + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + return await self._async_create_soundtouch_entry() + return self.async_show_form( + step_id="zeroconf_confirm", + last_step=True, + description_placeholders={"name": self.name}, + ) + + async def _async_get_device_id(self, raise_on_progress: bool = True) -> None: + """Get device ID from SoundTouch device.""" + device = await self.hass.async_add_executor_job(soundtouch_device, self.host) + + # Check if already configured + await self.async_set_unique_id( + device.config.device_id, raise_on_progress=raise_on_progress + ) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) + + self.name = device.config.name + + async def _async_create_soundtouch_entry(self): + """Finish config flow and create a SoundTouch config entry.""" + return self.async_create_entry( + title=self.name, + data={ + CONF_HOST: self.host, + }, + ) diff --git a/homeassistant/components/soundtouch/const.py b/homeassistant/components/soundtouch/const.py index 37bf1d8cc2b..a6b2b3c9f5f 100644 --- a/homeassistant/components/soundtouch/const.py +++ b/homeassistant/components/soundtouch/const.py @@ -1,4 +1,4 @@ -"""Constants for the Bose Soundtouch component.""" +"""Constants for the Bose SoundTouch component.""" DOMAIN = "soundtouch" SERVICE_PLAY_EVERYWHERE = "play_everywhere" SERVICE_CREATE_ZONE = "create_zone" diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index 15091ec04f7..4512f3a8f9b 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -1,10 +1,12 @@ { "domain": "soundtouch", - "name": "Bose Soundtouch", + "name": "Bose SoundTouch", "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": ["libsoundtouch==0.8"], - "after_dependencies": ["zeroconf"], - "codeowners": [], + "zeroconf": ["_soundtouch._tcp.local."], + "dependencies": ["repairs"], + "codeowners": ["@kroimon"], "iot_class": "local_polling", - "loggers": ["libsoundtouch"] + "loggers": ["libsoundtouch"], + "config_flow": true } diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index f8a5191d9db..74d89404d27 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -1,23 +1,26 @@ -"""Support for interface with a Bose Soundtouch.""" +"""Support for interface with a Bose SoundTouch.""" from __future__ import annotations from functools import partial import logging import re -from libsoundtouch import soundtouch_device +from libsoundtouch.device import SoundTouchDevice from libsoundtouch.utils import Source import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -27,19 +30,16 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ( - DOMAIN, - SERVICE_ADD_ZONE_SLAVE, - SERVICE_CREATE_ZONE, - SERVICE_PLAY_EVERYWHERE, - SERVICE_REMOVE_ZONE_SLAVE, -) +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -50,137 +50,66 @@ MAP_STATUS = { "STOP_STATE": STATE_OFF, } -DATA_SOUNDTOUCH = "soundtouch" ATTR_SOUNDTOUCH_GROUP = "soundtouch_group" ATTR_SOUNDTOUCH_ZONE = "soundtouch_zone" -SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({vol.Required("master"): cv.entity_id}) - -SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema( - {vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids} -) - -SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema( - {vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids} -) - -SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema( - {vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids} -) - -DEFAULT_NAME = "Bose Soundtouch" -DEFAULT_PORT = 8090 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_NAME, default=""): cv.string, + } + ), ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Bose Soundtouch platform.""" - if DATA_SOUNDTOUCH not in hass.data: - hass.data[DATA_SOUNDTOUCH] = [] - - if discovery_info: - host = discovery_info["host"] - port = int(discovery_info["port"]) - - # if device already exists by config - if host in [device.config["host"] for device in hass.data[DATA_SOUNDTOUCH]]: - return - - remote_config = {"id": "ha.component.soundtouch", "host": host, "port": port} - bose_soundtouch_entity = SoundTouchDevice(None, remote_config) - hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) - add_entities([bose_soundtouch_entity], True) - else: - name = config.get(CONF_NAME) - remote_config = { - "id": "ha.component.soundtouch", - "port": config.get(CONF_PORT), - "host": config.get(CONF_HOST), - } - bose_soundtouch_entity = SoundTouchDevice(name, remote_config) - hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) - add_entities([bose_soundtouch_entity], True) - - def service_handle(service: ServiceCall) -> None: - """Handle the applying of a service.""" - master_device_id = service.data.get("master") - slaves_ids = service.data.get("slaves") - slaves = [] - if slaves_ids: - slaves = [ - device - for device in hass.data[DATA_SOUNDTOUCH] - if device.entity_id in slaves_ids - ] - - master = next( - iter( - [ - device - for device in hass.data[DATA_SOUNDTOUCH] - if device.entity_id == master_device_id - ] - ), - None, + """Set up the Bose SoundTouch platform.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + _LOGGER.warning( + "Configuration of the Bose SoundTouch integration in YAML is " + "deprecated and will be removed in Home Assistant 2022.10; Your " + "existing configuration has been imported into the UI automatically " + "and can be safely removed from your configuration.yaml file" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - - if master is None: - _LOGGER.warning( - "Unable to find master with entity_id: %s", str(master_device_id) - ) - return - - if service.service == SERVICE_PLAY_EVERYWHERE: - slaves = [ - d for d in hass.data[DATA_SOUNDTOUCH] if d.entity_id != master_device_id - ] - master.create_zone(slaves) - elif service.service == SERVICE_CREATE_ZONE: - master.create_zone(slaves) - elif service.service == SERVICE_REMOVE_ZONE_SLAVE: - master.remove_zone_slave(slaves) - elif service.service == SERVICE_ADD_ZONE_SLAVE: - master.add_zone_slave(slaves) - - hass.services.register( - DOMAIN, - SERVICE_PLAY_EVERYWHERE, - service_handle, - schema=SOUNDTOUCH_PLAY_EVERYWHERE, - ) - hass.services.register( - DOMAIN, - SERVICE_CREATE_ZONE, - service_handle, - schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SERVICE_REMOVE_ZONE_SLAVE, - service_handle, - schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SERVICE_ADD_ZONE_SLAVE, - service_handle, - schema=SOUNDTOUCH_ADD_ZONE_SCHEMA, ) -class SoundTouchDevice(MediaPlayerEntity): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Bose SoundTouch media player based on a config entry.""" + device = hass.data[DOMAIN][entry.entry_id].device + media_player = SoundTouchMediaPlayer(device) + + async_add_entities([media_player], True) + + hass.data[DOMAIN][entry.entry_id].media_player = media_player + + +class SoundTouchMediaPlayer(MediaPlayerEntity): """Representation of a SoundTouch Bose device.""" _attr_supported_features = ( @@ -197,28 +126,32 @@ class SoundTouchDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.BROWSE_MEDIA ) + _attr_device_class = MediaPlayerDeviceClass.SPEAKER - def __init__(self, name, config): - """Create Soundtouch Entity.""" + def __init__(self, device: SoundTouchDevice) -> None: + """Create SoundTouch media player entity.""" + + self._device = device + + self._attr_unique_id = self._device.config.device_id + self._attr_name = self._device.config.name + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device.config.device_id)}, + connections={ + (CONNECTION_NETWORK_MAC, format_mac(self._device.config.mac_address)) + }, + manufacturer="Bose Corporation", + model=self._device.config.type, + name=self._device.config.name, + ) - self._device = soundtouch_device(config["host"], config["port"]) - if name is None: - self._name = self._device.config.name - else: - self._name = name self._status = None self._volume = None - self._config = config self._zone = None - @property - def config(self): - """Return specific soundtouch configuration.""" - return self._config - @property def device(self): - """Return Soundtouch device.""" + """Return SoundTouch device.""" return self._device def update(self): @@ -232,17 +165,15 @@ class SoundTouchDevice(MediaPlayerEntity): """Volume level of the media player (0..1).""" return self._volume.actual / 100 - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def state(self): """Return the state of the device.""" - if self._status.source == "STANDBY": + if self._status is None or self._status.source == "STANDBY": return STATE_OFF + if self._status.source == "INVALID_SOURCE": + return STATE_UNKNOWN + return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE) @property @@ -478,15 +409,12 @@ class SoundTouchDevice(MediaPlayerEntity): if not zone_status: return None - # Due to a bug in the SoundTouch API itself client devices do NOT return their - # siblings as part of the "slaves" list. Only the master has the full list of - # slaves for some reason. To compensate for this shortcoming we have to fetch - # the zone info from the master when the current device is a slave until this is - # fixed in the SoundTouch API or libsoundtouch, or of course until somebody has a - # better idea on how to fix this. + # Client devices do NOT return their siblings as part of the "slaves" list. + # Only the master has the full list of slaves. To compensate for this shortcoming + # we have to fetch the zone info from the master when the current device is a slave. # In addition to this shortcoming, libsoundtouch seems to report the "is_master" # property wrong on some slaves, so the only reliable way to detect if the current - # devices is the master, is by comparing the master_id of the zone with the device_id + # devices is the master, is by comparing the master_id of the zone with the device_id. if zone_status.master_id == self._device.config.device_id: return self._build_zone_info(self.entity_id, zone_status.slaves) @@ -505,16 +433,16 @@ class SoundTouchDevice(MediaPlayerEntity): def _get_instance_by_ip(self, ip_address): """Search and return a SoundTouchDevice instance by it's IP address.""" - for instance in self.hass.data[DATA_SOUNDTOUCH]: - if instance and instance.config["host"] == ip_address: - return instance + for data in self.hass.data[DOMAIN].values(): + if data.device.config.device_ip == ip_address: + return data.media_player return None def _get_instance_by_id(self, instance_id): """Search and return a SoundTouchDevice instance by it's ID (aka MAC address).""" - for instance in self.hass.data[DATA_SOUNDTOUCH]: - if instance and instance.device.config.device_id == instance_id: - return instance + for data in self.hass.data[DOMAIN].values(): + if data.device.config.device_id == instance_id: + return data.media_player return None def _build_zone_info(self, master, zone_slaves): diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml index 8d255e5f069..82709053496 100644 --- a/homeassistant/components/soundtouch/services.yaml +++ b/homeassistant/components/soundtouch/services.yaml @@ -1,6 +1,6 @@ play_everywhere: name: Play everywhere - description: Play on all Bose Soundtouch devices. + description: Play on all Bose SoundTouch devices. fields: master: name: Master @@ -13,7 +13,7 @@ play_everywhere: create_zone: name: Create zone - description: Create a Soundtouch multi-room zone. + description: Create a SoundTouch multi-room zone. fields: master: name: Master @@ -35,7 +35,7 @@ create_zone: add_zone_slave: name: Add zone slave - description: Add a slave to a Soundtouch multi-room zone. + description: Add a slave to a SoundTouch multi-room zone. fields: master: name: Master @@ -57,7 +57,7 @@ add_zone_slave: remove_zone_slave: name: Remove zone slave - description: Remove a slave from the Soundtouch multi-room zone. + description: Remove a slave from the SoundTouch multi-room zone. fields: master: name: Master diff --git a/homeassistant/components/soundtouch/strings.json b/homeassistant/components/soundtouch/strings.json new file mode 100644 index 00000000000..6a8896c8f56 --- /dev/null +++ b/homeassistant/components/soundtouch/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "zeroconf_confirm": { + "title": "Confirm adding Bose SoundTouch device", + "description": "You are about to add the SoundTouch device named `{name}` to Home Assistant." + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Bose SoundTouch YAML configuration is being removed", + "description": "Configuring Bose SoundTouch using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Bose SoundTouch YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/soundtouch/translations/ar.json b/homeassistant/components/soundtouch/translations/ar.json new file mode 100644 index 00000000000..22fdd032123 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ar.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0647\u0627\u0632 \u062a\u0645 \u0625\u0639\u062f\u0627\u062f\u0647 \u0645\u0633\u0628\u0642\u0627" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641" + } + }, + "zeroconf_confirm": { + "description": "\u0623\u0646\u062a \u0639\u0644\u0649 \u0648\u0634\u0643 \u0627\u0636\u0627\u0641\u0629 \u062c\u0647\u0627\u0632 SoundTouch \u0630\u0627\u062a \u0627\u0644\u0627\u0633\u0645 {name} \u0625\u0644\u0649 Home Assistant", + "title": "\u062c\u0627\u0631\u064a \u0625\u0636\u0627\u0641\u0629 \u062c\u0647\u0627\u0632 Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ca.json b/homeassistant/components/soundtouch/translations/ca.json new file mode 100644 index 00000000000..baba19644fb --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + }, + "zeroconf_confirm": { + "description": "Est\u00e0s a punt d'afegir el dispositiu SoundTouch amb nom `{name}` a Home Assistant.", + "title": "Confirma l'addici\u00f3 del dispositiu Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/de.json b/homeassistant/components/soundtouch/translations/de.json new file mode 100644 index 00000000000..379516e31be --- /dev/null +++ b/homeassistant/components/soundtouch/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Du bist im Begriff, das SoundTouch-Ger\u00e4t mit dem Namen `{name}` zu Home Assistant hinzuzuf\u00fcgen.", + "title": "Best\u00e4tige das Hinzuf\u00fcgen des Bose SoundTouch-Ger\u00e4ts" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Bose SoundTouch mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Bose SoundTouch YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Bose SoundTouch YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/el.json b/homeassistant/components/soundtouch/translations/el.json new file mode 100644 index 00000000000..7346aeca660 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + }, + "zeroconf_confirm": { + "description": "\u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae SoundTouch \u03bc\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 `{name}` \u03c3\u03c4\u03bf Home Assistant.", + "title": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/en.json b/homeassistant/components/soundtouch/translations/en.json new file mode 100644 index 00000000000..28eb32b7d43 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "You are about to add the SoundTouch device named `{name}` to Home Assistant.", + "title": "Confirm adding Bose SoundTouch device" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Bose SoundTouch using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Bose SoundTouch YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Bose SoundTouch YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/et.json b/homeassistant/components/soundtouch/translations/et.json new file mode 100644 index 00000000000..0adb9ddba8b --- /dev/null +++ b/homeassistant/components/soundtouch/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Oled lisamas SoundTouchi seadet nimega `{name}` Home Assistantisse.", + "title": "Kinnita Bose SoundTouchi seadme lisamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/fr.json b/homeassistant/components/soundtouch/translations/fr.json new file mode 100644 index 00000000000..e0e187756af --- /dev/null +++ b/homeassistant/components/soundtouch/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + }, + "zeroconf_confirm": { + "description": "Vous \u00eates sur le point d'ajouter l'appareil SoundTouch nomm\u00e9 `{name}` \u00e0 Home Assistant.", + "title": "Confirmer l'ajout de l'appareil Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/hu.json b/homeassistant/components/soundtouch/translations/hu.json new file mode 100644 index 00000000000..a0f5c364af6 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + }, + "zeroconf_confirm": { + "description": "Hamarosan hozz\u00e1adja a \"{name}\" nev\u0171 SoundTouch eszk\u00f6zt a Home Assistanthoz.", + "title": "Bose SoundTouch eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1nak meger\u0151s\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json new file mode 100644 index 00000000000..ce2c8f4e1a3 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Anda akan menambahkan perangkat SoundTouch bernama `{name}` ke Home Assistant.", + "title": "Konfirmasi penambahan perangkat Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/it.json b/homeassistant/components/soundtouch/translations/it.json new file mode 100644 index 00000000000..f9c6d512b2a --- /dev/null +++ b/homeassistant/components/soundtouch/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Stai per aggiungere il dispositivo SoundTouch denominato `{name}` a Home Assistant.", + "title": "Conferma l'aggiunta del dispositivo Bose SoundTouch" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Bose SoundTouch tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Bose SoundTouch dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Bose SoundTouch verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ja.json b/homeassistant/components/soundtouch/translations/ja.json new file mode 100644 index 00000000000..c4a94a23a7e --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + }, + "zeroconf_confirm": { + "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eSoundTouch\u30c7\u30d0\u30a4\u30b9\u3092\u3001Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002", + "title": "Bose SoundTouch\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ko.json b/homeassistant/components/soundtouch/translations/ko.json new file mode 100644 index 00000000000..fc4995bcf2c --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/nl.json b/homeassistant/components/soundtouch/translations/nl.json new file mode 100644 index 00000000000..0ccc8057ac8 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/no.json b/homeassistant/components/soundtouch/translations/no.json new file mode 100644 index 00000000000..bdaee03da54 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + }, + "zeroconf_confirm": { + "description": "Du er i ferd med \u00e5 legge til SoundTouch-enheten med navnet ` {name} ` til Home Assistant.", + "title": "Bekreft \u00e5 legge til Bose SoundTouch-enhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pl.json b/homeassistant/components/soundtouch/translations/pl.json new file mode 100644 index 00000000000..10a760e1acf --- /dev/null +++ b/homeassistant/components/soundtouch/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + }, + "zeroconf_confirm": { + "description": "Zamierzasz doda\u0107 urz\u0105dzenie SoundTouch o nazwie `{name}` do Home Assistanta.", + "title": "Potwierd\u017a dodanie urz\u0105dzenia Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt-BR.json b/homeassistant/components/soundtouch/translations/pt-BR.json new file mode 100644 index 00000000000..7446cbc5a06 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Voc\u00ea est\u00e1 prestes a adicionar o dispositivo SoundTouch chamado ` {name} ` ao Home Assistant.", + "title": "Confirme a adi\u00e7\u00e3o do dispositivo Bose SoundTouch" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Bose SoundTouch usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Bose SoundTouch do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Bose SoundTouch est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt.json b/homeassistant/components/soundtouch/translations/pt.json new file mode 100644 index 00000000000..91786f4b324 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ru.json b/homeassistant/components/soundtouch/translations/ru.json new file mode 100644 index 00000000000..d987f817a85 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 Home Assistant \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e SoundTouch \u0441 \u0438\u043c\u0435\u043d\u0435\u043c `{name}`.", + "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/tr.json b/homeassistant/components/soundtouch/translations/tr.json new file mode 100644 index 00000000000..957215dd8b3 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + }, + "zeroconf_confirm": { + "description": "` {name} ` adl\u0131 SoundTouch cihaz\u0131n\u0131 Home Assistant'a eklemek \u00fczeresiniz.", + "title": "Bose SoundTouch cihaz\u0131 eklemeyi onaylay\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json new file mode 100644 index 00000000000..f3d8e8e8560 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + }, + "zeroconf_confirm": { + "description": "\u6b63\u8981\u65b0\u589e\u540d\u70ba `{name}` \u7684 SoundTouch \u88dd\u7f6e\u81f3 Home Assistant\u3002", + "title": "\u78ba\u8a8d\u65b0\u589e Bose SoundTouch \u88dd\u7f6e" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Bose SoundTouch YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 2cbb5e405fb..00221c39a42 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 4f16c012fa6..44b018e1e19 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -49,6 +49,7 @@ class SpeedtestSensor( """Implementation of a speedtest.net sensor.""" entity_description: SpeedtestSensorEntityDescription + _attr_has_entity_name = True _attr_icon = ICON def __init__( @@ -59,7 +60,6 @@ class SpeedtestSensor( """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" self._attr_unique_id = description.key self._state: StateType = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index 5712139b6b9..40f592b2c46 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index b3c1918aad4..d4122a91d62 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/spider/translations/ja.json b/homeassistant/components/spider/translations/ja.json index 9277adceeee..f45ec1ebb98 100644 --- a/homeassistant/components/spider/translations/ja.json +++ b/homeassistant/components/spider/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index bf4e9d8deae..83841a1780e 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -3,25 +3,15 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -import logging from typing import Any import aiohttp import requests from spotipy import Spotify, SpotifyException -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_CREDENTIALS, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - Platform, -) +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -40,23 +30,7 @@ from .util import ( spotify_uri_from_media_browser_url, ) -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Inclusive(CONF_CLIENT_ID, ATTR_CREDENTIALS): cv.string, - vol.Inclusive(CONF_CLIENT_SECRET, ATTR_CREDENTIALS): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER] @@ -81,24 +55,15 @@ class HomeAssistantSpotifyData: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Spotify integration.""" - if DOMAIN not in config: - return True - - if CONF_CLIENT_ID in config[DOMAIN]: - await async_import_client_credential( + if DOMAIN in config: + async_create_issue( hass, DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of Spotify integration in YAML is deprecated and " - "will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", ) return True @@ -165,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): raise ConfigEntryAuthFailed - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 2940700d230..0556cad26b7 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/spotify", "requirements": ["spotipy==2.20.0"], "zeroconf": ["_spotify-connect._tcp.local."], - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "codeowners": ["@frenck"], "config_flow": true, "quality_scale": "silver", diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 7d24f1deee8..04f523c2d4b 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -107,10 +107,11 @@ def spotify_exception_handler(func): class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" + _attr_entity_registry_enabled_default = False + _attr_has_entity_name = True _attr_icon = "mdi:spotify" _attr_media_content_type = MEDIA_TYPE_MUSIC _attr_media_image_remotely_accessible = False - _attr_entity_registry_enabled_default = False def __init__( self, @@ -122,7 +123,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self._id = user_id self.data = data - self._attr_name = f"Spotify {name}" self._attr_unique_id = user_id if self.data.current_user["product"] == "premium": diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index caec5b8a288..4405bd21310 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -21,5 +21,11 @@ "info": { "api_endpoint_reachable": "Spotify API endpoint reachable" } + }, + "issues": { + "removed_yaml": { + "title": "The Spotify YAML configuration has been removed", + "description": "Configuring Spotify using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index 799f3717b81..9d48e7b58e2 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Spotify \u00fcber YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird von Home Assistant nicht verwendet.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Spotify-YAML-Konfiguration wurde entfernt" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify-API-Endpunkt erreichbar" diff --git a/homeassistant/components/spotify/translations/el.json b/homeassistant/components/spotify/translations/el.json index 1b9aadb7caf..1c89861b325 100644 --- a/homeassistant/components/spotify/translations/el.json +++ b/homeassistant/components/spotify/translations/el.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Spotify \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Spotify YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + }, "system_health": { "info": { "api_endpoint_reachable": "\u03a4\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c4\u03bf\u03c5 Spotify API \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf" diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json index 7136e5a8e71..0ccebc9833b 100644 --- a/homeassistant/components/spotify/translations/en.json +++ b/homeassistant/components/spotify/translations/en.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Configuring Spotify using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Spotify YAML configuration has been removed" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API endpoint reachable" diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index f387ff6bb3a..735c4942fb2 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "reauth_account_mismatch": "A Spotify-fi\u00f3kkal hiteles\u00edtett fi\u00f3k nem egyezik meg az \u00faj hiteles\u00edt\u00e9shez sz\u00fcks\u00e9ges fi\u00f3kkal." }, diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index 445b7a233e4..2ca8d323607 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Spotify tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Spotify \u00e8 stata rimossa" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Endpoint API Spotify raggiungibile" diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index 9c0e308dc18..4a65f5037dd 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "missing_configuration": "Spotify\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_account_mismatch": "\u8a8d\u8a3c\u3055\u308c\u305fSpotify\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u3001\u518d\u8a8d\u8a3c\u304c\u5fc5\u8981\u306a\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, @@ -14,8 +14,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Spotify\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {account}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Spotify\u7d71\u5408\u3067\u306f\u3001Spotify\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {account}", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json index 52028d4d368..bbdda0ae586 100644 --- a/homeassistant/components/spotify/translations/pl.json +++ b/homeassistant/components/spotify/translations/pl.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Spotify za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Spotify zosta\u0142a usuni\u0119ta" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Punkt ko\u0144cowy Spotify API osi\u0105galny" diff --git a/homeassistant/components/spotify/translations/pt-BR.json b/homeassistant/components/spotify/translations/pt-BR.json index c49fbfabfa0..97948f71641 100644 --- a/homeassistant/components/spotify/translations/pt-BR.json +++ b/homeassistant/components/spotify/translations/pt-BR.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Spotify usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Spotify YAML foi removida" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Endpoint da API do Spotify acess\u00edvel" diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index fc9362697e2..35918b2634f 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Spotify \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } + }, "system_health": { "info": { "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a API Spotify" diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index ce35507e661..52773f3e411 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Spotify YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API \u53ef\u9054\u7aef\u9ede" diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index 2917056b5d4..63f81c784be 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SQL from a config entry.""" entry.async_on_unload(entry.add_update_listener(async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 33ddafe2c0a..dfb1e15f052 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -145,6 +145,7 @@ class SQLSensor(SensorEntity): """Representation of an SQL sensor.""" _attr_icon = "mdi:database-search" + _attr_has_entity_name = True def __init__( self, @@ -157,7 +158,6 @@ class SQLSensor(SensorEntity): entry_id: str, ) -> None: """Initialize the SQL sensor.""" - self._attr_name = name self._query = query self._attr_native_unit_of_measurement = unit self._template = value_template diff --git a/homeassistant/components/sql/translations/pt.json b/homeassistant/components/sql/translations/pt.json new file mode 100644 index 00000000000..f2cf791e9fe --- /dev/null +++ b/homeassistant/components/sql/translations/pt.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "data": { + "unit_of_measurement": "Unidade de Medida" + }, + "data_description": { + "unit_of_measurement": "Unidade de Medida (opcional)" + } + } + } + }, + "options": { + "error": { + "query_invalid": "Busca SQL In\u00e1lida" + }, + "step": { + "init": { + "data": { + "column": "Coluna", + "unit_of_measurement": "Unidade de Medida" + }, + "data_description": { + "unit_of_measurement": "Unidade de Medida (opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index 89b3300fc5a..b3e2717d075 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -15,7 +15,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Logitech Squeezebox from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/squeezebox/translations/pt.json b/homeassistant/components/squeezebox/translations/pt.json index 97ced548ed5..86aa270e47d 100644 --- a/homeassistant/components/squeezebox/translations/pt.json +++ b/homeassistant/components/squeezebox/translations/pt.json @@ -17,7 +17,8 @@ "password": "Palavra-passe", "port": "Porta", "username": "Utilizador" - } + }, + "title": "Editar informa\u00e7\u00e3o sobre a liga\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index ac9cf693c10..bed6780e523 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Unable to connect to Srp Energy: %s", str(ex)) raise ConfigEntryNotReady from ex - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json index 805a500502b..2e33e78e8d8 100644 --- a/homeassistant/components/srp_energy/translations/ja.json +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/srp_energy/translations/pt.json b/homeassistant/components/srp_energy/translations/pt.json index 3e10b977773..15c3b188dec 100644 --- a/homeassistant/components/srp_energy/translations/pt.json +++ b/homeassistant/components/srp_energy/translations/pt.json @@ -5,12 +5,14 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_account": "O ID da conta deve ser um n\u00famero de 9 d\u00edgitos", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { + "id": "ID da conta", "password": "Palavra-passe", "username": "Nome de Utilizador" } diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 886a583ae93..2af7b4a75f4 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config_entry_id=entry.entry_id, **account.device_info(device) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def async_set_scan_interval(call: ServiceCall) -> None: """Set scan interval.""" diff --git a/homeassistant/components/starline/translations/de.json b/homeassistant/components/starline/translations/de.json index 87a9249475e..22e60dd10c0 100644 --- a/homeassistant/components/starline/translations/de.json +++ b/homeassistant/components/starline/translations/de.json @@ -26,7 +26,7 @@ "mfa_code": "SMS Code" }, "description": "Gib den an das Telefon gesendeten Code ein {Telefon_Nummer}", - "title": "2-Faktor-Authentifizierung" + "title": "Zwei-Faktor-Authentifizierung" }, "auth_user": { "data": { diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index eae81dd8435..c422269a277 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -1,11 +1,13 @@ """The Steam integration.""" from __future__ import annotations +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN @@ -14,6 +16,22 @@ from .coordinator import SteamDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Steam integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Steam from a config entry.""" coordinator = SteamDataUpdateCoordinator(hass) diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 7ed1f0a3610..8356ad8bbc6 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -12,29 +12,16 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_ACCOUNT, - CONF_ACCOUNTS, - DEFAULT_NAME, - DOMAIN, - LOGGER, - PLACEHOLDERS, -) +from .const import CONF_ACCOUNT, CONF_ACCOUNTS, DOMAIN, LOGGER, PLACEHOLDERS -def validate_input( - user_input: dict[str, str | int], multi: bool = False -) -> list[dict[str, str | int]]: +def validate_input(user_input: dict[str, str]) -> dict[str, str | int]: """Handle common flow input validation.""" steam.api.key.set(user_input[CONF_API_KEY]) interface = steam.api.interface("ISteamUser") - if multi: - names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNTS]) - else: - names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNT]) - return names["response"]["players"]["player"] + names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNT]) + return names["response"]["players"]["player"][0] class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -62,8 +49,8 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): elif user_input is not None: try: res = await self.hass.async_add_executor_job(validate_input, user_input) - if res[0] is not None: - name = str(res[0]["personaname"]) + if res is not None: + name = str(res["personaname"]) else: errors["base"] = "invalid_account" except (steam.api.HTTPError, steam.api.HTTPTimeoutError) as ex: @@ -80,22 +67,10 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="reauth_successful") self._abort_if_unique_id_configured() - if self.source == config_entries.SOURCE_IMPORT: - res = await self.hass.async_add_executor_job( - validate_input, user_input, True - ) - accounts_data = { - CONF_ACCOUNTS: { - acc["steamid"]: acc["personaname"] for acc in res - } - } - user_input.pop(CONF_ACCOUNTS) - else: - accounts_data = {CONF_ACCOUNTS: {user_input[CONF_ACCOUNT]: name}} return self.async_create_entry( - title=name or DEFAULT_NAME, + title=name, data=user_input, - options=accounts_data, + options={CONF_ACCOUNTS: {user_input[CONF_ACCOUNT]: name}}, ) user_input = user_input or {} return self.async_show_form( @@ -114,18 +89,6 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=PLACEHOLDERS, ) - async def async_step_import(self, import_config: ConfigType) -> FlowResult: - """Import a config entry from configuration.yaml.""" - for entry in self._async_current_entries(): - if entry.data[CONF_API_KEY] == import_config[CONF_API_KEY]: - return self.async_abort(reason="already_configured") - LOGGER.warning( - "Steam yaml config is now deprecated and has been imported. " - "Please remove it from your config" - ) - import_config[CONF_ACCOUNT] = import_config[CONF_ACCOUNTS][0] - return await self.async_step_user(import_config) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index f8aba1aee07..f2e3a35bbe7 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/steam_online", "requirements": ["steamodd==4.21"], + "dependencies": ["repairs"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["steam"] diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index be175b41b66..307dfac0542 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -4,19 +4,11 @@ from __future__ import annotations from datetime import datetime from time import localtime, mktime -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utc_from_timestamp from . import SteamEntity @@ -31,31 +23,9 @@ from .const import ( ) from .coordinator import SteamDataUpdateCoordinator -# Deprecated in Home Assistant 2022.5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_ACCOUNTS, default=[]): vol.All(cv.ensure_list, [cv.string]), - } -) - PARALLEL_UPDATES = 1 -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Twitch sensor from yaml.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/homeassistant/components/steam_online/strings.json b/homeassistant/components/steam_online/strings.json index 1b431795ea4..63dc7cce22a 100644 --- a/homeassistant/components/steam_online/strings.json +++ b/homeassistant/components/steam_online/strings.json @@ -35,5 +35,11 @@ "error": { "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" } + }, + "issues": { + "removed_yaml": { + "title": "The Steam YAML configuration has been removed", + "description": "Configuring Steam using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/steam_online/translations/cs.json b/homeassistant/components/steam_online/translations/cs.json new file mode 100644 index 00000000000..ffd864a8f1d --- /dev/null +++ b/homeassistant/components/steam_online/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/de.json b/homeassistant/components/steam_online/translations/de.json index 44fb7f8b08b..aef08bfe269 100644 --- a/homeassistant/components/steam_online/translations/de.json +++ b/homeassistant/components/steam_online/translations/de.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Steam \u00fcber YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird von Home Assistant nicht verwendet.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Steam-YAML-Konfiguration wurde entfernt" + } + }, "options": { "error": { "unauthorized": "Freundesliste eingeschr\u00e4nkt: Bitte lies in der Dokumentation nach, wie du alle anderen Freunde sehen kannst" diff --git a/homeassistant/components/steam_online/translations/el.json b/homeassistant/components/steam_online/translations/el.json index 02405dc0215..33a864e0945 100644 --- a/homeassistant/components/steam_online/translations/el.json +++ b/homeassistant/components/steam_online/translations/el.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Steam \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd Steam YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + }, "options": { "error": { "unauthorized": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c6\u03af\u03bb\u03c9\u03bd: \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03ac\u03bb\u03bb\u03bf\u03c5\u03c2 \u03c6\u03af\u03bb\u03bf\u03c5\u03c2" diff --git a/homeassistant/components/steam_online/translations/en.json b/homeassistant/components/steam_online/translations/en.json index 7226c5ee177..2a7bef3f683 100644 --- a/homeassistant/components/steam_online/translations/en.json +++ b/homeassistant/components/steam_online/translations/en.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Configuring Steam using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Steam YAML configuration has been removed" + } + }, "options": { "error": { "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" diff --git a/homeassistant/components/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json index a7d6c6a7de4..87e78abedb4 100644 --- a/homeassistant/components/steam_online/translations/hu.json +++ b/homeassistant/components/steam_online/translations/hu.json @@ -26,7 +26,7 @@ }, "options": { "error": { - "unauthorized": "Bar\u00e1ti lista korl\u00e1tozott: K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t arr\u00f3l, hogyan l\u00e1thatja az \u00f6sszes t\u00f6bbi bar\u00e1tot." + "unauthorized": "A bar\u00e1t-lista korl\u00e1tozva van: K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t arr\u00f3l, hogyan l\u00e1thatja az \u00f6sszes t\u00f6bbi bar\u00e1tj\u00e1t." }, "step": { "init": { diff --git a/homeassistant/components/steam_online/translations/it.json b/homeassistant/components/steam_online/translations/it.json index 3f2e9481301..fb962124f2a 100644 --- a/homeassistant/components/steam_online/translations/it.json +++ b/homeassistant/components/steam_online/translations/it.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Steam tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Steam \u00e8 stata rimossa" + } + }, "options": { "error": { "unauthorized": "Elenco amici limitato: consultare la documentazione su come vedere tutti gli altri amici." diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index e138d46319b..f62fd45e767 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -12,8 +12,8 @@ }, "step": { "reauth_confirm": { - "description": "Steam\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\n\n\u3053\u3053\u3067\u3042\u306a\u305f\u306e\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059: {api_key_url}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Steam\u7d71\u5408\u306f\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\n\n\u3053\u3053\u3067\u3042\u306a\u305f\u306e\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059: {api_key_url}", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/steam_online/translations/pl.json b/homeassistant/components/steam_online/translations/pl.json index 3ceff9e442f..08c05b7de41 100644 --- a/homeassistant/components/steam_online/translations/pl.json +++ b/homeassistant/components/steam_online/translations/pl.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Steam za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Steam zosta\u0142a usuni\u0119ta" + } + }, "options": { "error": { "unauthorized": "Lista znajomych ograniczona: zapoznaj si\u0119 z dokumentacj\u0105, aby dowiedzie\u0107 si\u0119, jak zobaczy\u0107 wszystkich innych znajomych" diff --git a/homeassistant/components/steam_online/translations/pt-BR.json b/homeassistant/components/steam_online/translations/pt-BR.json index 5aa9d0d2520..9afd81f4a5f 100644 --- a/homeassistant/components/steam_online/translations/pt-BR.json +++ b/homeassistant/components/steam_online/translations/pt-BR.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o da Steam usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o da Steam YAML foi removida" + } + }, "options": { "error": { "unauthorized": "Lista restrita de amigos: consulte a documenta\u00e7\u00e3o sobre como ver todos os outros amigos" diff --git a/homeassistant/components/steam_online/translations/pt.json b/homeassistant/components/steam_online/translations/pt.json new file mode 100644 index 00000000000..18ac9fb4b40 --- /dev/null +++ b/homeassistant/components/steam_online/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index 0da65eda473..b69b1a72696 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -24,7 +24,16 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Steam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } + }, "options": { + "error": { + "unauthorized": "\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0440\u0443\u0437\u0435\u0439 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0432\u0441\u0435\u0445 \u0434\u0440\u0443\u0437\u044c\u044f\u0445." + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json index 7de1d6f1a3c..8b0c932735b 100644 --- a/homeassistant/components/steam_online/translations/zh-Hant.json +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Steam YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, "options": { "error": { "unauthorized": "\u597d\u53cb\u5217\u8868\u53d7\u9650\uff1a\u8acb\u53c3\u8003\u6587\u4ef6\u8cc7\u6599\u4ee5\u4e86\u89e3\u5982\u4f55\u986f\u793a\u6240\u6709\u597d\u53cb\u3002" diff --git a/homeassistant/components/steamist/__init__.py b/homeassistant/components/steamist/__init__.py index edd805e89d3..0a363f77e82 100644 --- a/homeassistant/components/steamist/__init__.py +++ b/homeassistant/components/steamist/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if discovery := await async_discover_device(hass, host): async_update_entry_from_discovery(hass, entry, discovery) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/steamist/translations/pt.json b/homeassistant/components/steamist/translations/pt.json new file mode 100644 index 00000000000..6ead5b65917 --- /dev/null +++ b/homeassistant/components/steamist/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/__init__.py b/homeassistant/components/stookalert/__init__.py index ad800c20e95..63458a2f78a 100644 --- a/homeassistant/components/stookalert/__init__.py +++ b/homeassistant/components/stookalert/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Stookalert from a config entry.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = stookalert.stookalert(entry.data[CONF_PROVINCE]) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index 974635e2efd..70a25c2bfdf 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -35,15 +35,15 @@ class StookalertBinarySensor(BinarySensorEntity): _attr_attribution = "Data provided by rivm.nl" _attr_device_class = BinarySensorDeviceClass.SAFETY + _attr_has_entity_name = True def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: """Initialize a Stookalert device.""" self._client = client - self._attr_name = f"Stookalert {entry.data[CONF_PROVINCE]}" self._attr_unique_id = entry.unique_id self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{entry.entry_id}")}, - name=entry.data[CONF_PROVINCE], + name=f"Stookalert {entry.data[CONF_PROVINCE]}", manufacturer="RIVM", model="Stookalert", entry_type=DeviceEntryType.SERVICE, diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index e9411e53224..6c090b93ac2 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.6", "ha-av==10.0.0b4"], + "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0b4"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index b33a5fbbf84..3bda8acfa7b 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -4,6 +4,7 @@ from __future__ import annotations from io import BytesIO import logging import os +from typing import TYPE_CHECKING import av @@ -16,6 +17,9 @@ from .const import ( ) from .core import PROVIDERS, IdleTimer, Segment, StreamOutput, StreamSettings +if TYPE_CHECKING: + import deque + _LOGGER = logging.getLogger(__name__) @@ -139,6 +143,25 @@ class RecorderOutput(StreamOutput): source.close() + def finish_writing( + segments: deque[Segment], output: av.OutputContainer, video_path: str + ) -> None: + """Finish writing output.""" + # Should only have 0 or 1 segments, but loop through just in case + while segments: + write_segment(segments.popleft()) + if output is None: + _LOGGER.error("Recording failed to capture anything") + return + output.close() + try: + os.rename(video_path + ".tmp", video_path) + except FileNotFoundError: + _LOGGER.error( + "Error writing to '%s'. There are likely multiple recordings writing to the same file", + video_path, + ) + # Write lookback segments while len(self._segments) > 1: # The last segment is in progress await self._hass.async_add_executor_job( @@ -153,14 +176,7 @@ class RecorderOutput(StreamOutput): await self._hass.async_add_executor_job( write_segment, self._segments.popleft() ) - # Write remaining segments - # Should only have 0 or 1 segments, but loop through just in case - while self._segments: - await self._hass.async_add_executor_job( - write_segment, self._segments.popleft() - ) - if output is None: - _LOGGER.error("Recording failed to capture anything") - else: - output.close() - os.rename(self.video_path + ".tmp", self.video_path) + # Write remaining segments and close output + await self._hass.async_add_executor_job( + finish_writing, self._segments, output, self.video_path + ) diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 6e05586706f..83a5a1c1c9d 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ENTRY_VEHICLES: vehicle_info, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index 9031d8b47ce..212303991b0 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { diff --git a/homeassistant/components/subaru/translations/hu.json b/homeassistant/components/subaru/translations/hu.json index 42f9f6bb2c9..9194b4e5044 100644 --- a/homeassistant/components/subaru/translations/hu.json +++ b/homeassistant/components/subaru/translations/hu.json @@ -11,14 +11,14 @@ "incorrect_pin": "Helytelen PIN", "incorrect_validation_code": "Helytelen \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3d", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "two_factor_request_failed": "A k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3d lek\u00e9r\u00e9se sikertelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra" + "two_factor_request_failed": "A k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3d lek\u00e9r\u00e9se sikertelen, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra." }, "step": { "pin": { "data": { "pin": "PIN" }, - "description": "K\u00e9rj\u00fck, adja meg MySubaru PIN-k\u00f3dj\u00e1t\n MEGJEGYZ\u00c9S: A sz\u00e1ml\u00e1n szerepl\u0151 \u00f6sszes j\u00e1rm\u0171nek azonos PIN-k\u00f3ddal kell rendelkeznie", + "description": "K\u00e9rem, adja meg MySubaru PIN-k\u00f3dj\u00e1t\nMEGJEGYZ\u00c9S: A fi\u00f3kban szerepl\u0151 \u00f6sszes j\u00e1rm\u0171nek azonos PIN-k\u00f3ddal kell rendelkeznie", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" }, "two_factor": { @@ -32,7 +32,7 @@ "data": { "validation_code": "\u00c9rv\u00e9nyes\u00edt\u00e9si k\u00f3d" }, - "description": "K\u00e9rj\u00fck, adja meg az \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3dot", + "description": "K\u00e9rem, adja meg az \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3dot", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" }, "user": { @@ -41,7 +41,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg MySubaru hiteles\u00edt\u0151 adatait\n MEGJEGYZ\u00c9S: A kezdeti be\u00e1ll\u00edt\u00e1s ak\u00e1r 30 m\u00e1sodpercet is ig\u00e9nybe vehetnek", + "description": "K\u00e9rem, adja meg MySubaru hiteles\u00edt\u0151 adatait.\nMEGJEGYZ\u00c9S: A kezdeti be\u00e1ll\u00edt\u00e1s ak\u00e1r 30 m\u00e1sodpercet is ig\u00e9nybe vehetnek.", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" } } diff --git a/homeassistant/components/sun/translations/ja.json b/homeassistant/components/sun/translations/ja.json index 8188e950389..ecb23ee53d6 100644 --- a/homeassistant/components/sun/translations/ja.json +++ b/homeassistant/components/sun/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/sun/translations/pt.json b/homeassistant/components/sun/translations/pt.json index 2f060112a0c..d050eb7c5f5 100644 --- a/homeassistant/components/sun/translations/pt.json +++ b/homeassistant/components/sun/translations/pt.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + }, "state": { "_": { "above_horizon": "Acima do horizonte", diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 75ea1306ccf..2e6125bc502 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) lock_state_service_schema = vol.Schema( { diff --git a/homeassistant/components/surepetcare/translations/pt.json b/homeassistant/components/surepetcare/translations/pt.json new file mode 100644 index 00000000000..565b9f6c0e8 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 1adeace7a96..102319cec93 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_id = async_add_to_device(hass, entry, entity_id) - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, (entry.options[CONF_TARGET_DOMAIN],) ) return True diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 68fbd4bd584..e32252a7615 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -1,88 +1,103 @@ """Support for Switchbot devices.""" +import logging + import switchbot +from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_SENSOR_TYPE, Platform +from homeassistant.const import ( + CONF_ADDRESS, + CONF_MAC, + CONF_PASSWORD, + CONF_SENSOR_TYPE, + Platform, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from .const import ( ATTR_BOT, + ATTR_CONTACT, ATTR_CURTAIN, - COMMON_OPTIONS, + ATTR_HYGROMETER, + ATTR_PLUG, CONF_RETRY_COUNT, - CONF_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT, - CONF_TIME_BETWEEN_UPDATE_COMMAND, - DATA_COORDINATOR, DEFAULT_RETRY_COUNT, - DEFAULT_RETRY_TIMEOUT, - DEFAULT_SCAN_TIMEOUT, - DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, DOMAIN, ) from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], + ATTR_PLUG: [Platform.SWITCH, Platform.SENSOR], ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], + ATTR_HYGROMETER: [Platform.SENSOR], + ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], } +CLASS_BY_DEVICE = { + ATTR_CURTAIN: switchbot.SwitchbotCurtain, + ATTR_BOT: switchbot.Switchbot, + ATTR_PLUG: switchbot.SwitchbotPlugMini, +} + +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Switchbot from a config entry.""" hass.data.setdefault(DOMAIN, {}) - - if not entry.options: - options = { - CONF_TIME_BETWEEN_UPDATE_COMMAND: DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, - CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT, - CONF_RETRY_TIMEOUT: DEFAULT_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT: DEFAULT_SCAN_TIMEOUT, - } - - hass.config_entries.async_update_entry(entry, options=options) - - # Use same coordinator instance for all entities. - # Uses BTLE advertisement data, all Switchbot devices in range is stored here. - if DATA_COORDINATOR not in hass.data[DOMAIN]: - - if COMMON_OPTIONS not in hass.data[DOMAIN]: - hass.data[DOMAIN][COMMON_OPTIONS] = {**entry.options} - - switchbot.DEFAULT_RETRY_TIMEOUT = hass.data[DOMAIN][COMMON_OPTIONS][ - CONF_RETRY_TIMEOUT - ] - - # Store api in coordinator. - coordinator = SwitchbotDataUpdateCoordinator( - hass, - update_interval=hass.data[DOMAIN][COMMON_OPTIONS][ - CONF_TIME_BETWEEN_UPDATE_COMMAND - ], - api=switchbot, - retry_count=hass.data[DOMAIN][COMMON_OPTIONS][CONF_RETRY_COUNT], - scan_timeout=hass.data[DOMAIN][COMMON_OPTIONS][CONF_SCAN_TIMEOUT], + if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data: + # Bleak uses addresses not mac addresses which are are actually + # UUIDs on some platforms (MacOS). + mac = entry.data[CONF_MAC] + if "-" not in mac: + mac = dr.format_mac(mac) + hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_ADDRESS: mac}, ) - hass.data[DOMAIN][DATA_COORDINATOR] = coordinator + if not entry.options: + hass.config_entries.async_update_entry( + entry, + options={CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT}, + ) - else: - coordinator = hass.data[DOMAIN][DATA_COORDINATOR] - - await coordinator.async_config_entry_first_refresh() + sensor_type: str = entry.data[CONF_SENSOR_TYPE] + address: str = entry.data[CONF_ADDRESS] + ble_device = bluetooth.async_ble_device_from_address(hass, address.upper()) + if not ble_device: + raise ConfigEntryNotReady( + f"Could not find Switchbot {sensor_type} with address {address}" + ) + cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) + device = cls( + device=ble_device, + password=entry.data.get(CONF_PASSWORD), + retry_count=entry.options[CONF_RETRY_COUNT], + ) + coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator( + hass, _LOGGER, ble_device, device + ) + entry.async_on_unload(coordinator.async_start()) + if not await coordinator.async_wait_ready(): + raise ConfigEntryNotReady(f"Switchbot {sensor_type} with {address} not ready") entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - - hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator} - - sensor_type = entry.data[CONF_SENSOR_TYPE] - - hass.config_entries.async_setup_platforms(entry, PLATFORMS_BY_TYPE[sensor_type]) + await hass.config_entries.async_forward_entry_setups( + entry, PLATFORMS_BY_TYPE[sensor_type] + ) return True +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" sensor_type = entry.data[CONF_SENSOR_TYPE] @@ -92,18 +107,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - - if len(hass.config_entries.async_entries(DOMAIN)) == 0: + if not hass.config_entries.async_entries(DOMAIN): hass.data.pop(DOMAIN) return unload_ok - - -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - # Update entity options stored in hass. - if {**entry.options} != hass.data[DOMAIN][COMMON_OPTIONS]: - hass.data[DOMAIN][COMMON_OPTIONS] = {**entry.options} - hass.data[DOMAIN].pop(DATA_COORDINATOR) - - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index e2a5a951d1d..4da4ed531b0 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -2,17 +2,17 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -21,8 +21,30 @@ PARALLEL_UPDATES = 1 BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "calibration": BinarySensorEntityDescription( key="calibration", + name="Calibration", entity_category=EntityCategory.DIAGNOSTIC, ), + "motion_detected": BinarySensorEntityDescription( + key="pir_state", + name="Motion detected", + device_class=BinarySensorDeviceClass.MOTION, + ), + "contact_open": BinarySensorEntityDescription( + key="contact_open", + name="Door open", + device_class=BinarySensorDeviceClass.DOOR, + ), + "contact_timeout": BinarySensorEntityDescription( + key="contact_timeout", + name="Door timeout", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + "is_light": BinarySensorEntityDescription( + key="is_light", + name="Light", + device_class=BinarySensorDeviceClass.LIGHT, + ), } @@ -30,23 +52,19 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Switchbot curtain based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotBinarySensor( coordinator, - entry.unique_id, + unique_id, binary_sensor, - entry.data[CONF_MAC], + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], ) - for binary_sensor in coordinator.data[entry.unique_id]["data"] + for binary_sensor in coordinator.data["data"] if binary_sensor in BINARY_SENSOR_TYPES ] ) @@ -55,20 +73,22 @@ async def async_setup_entry( class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): """Representation of a Switchbot binary sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, + unique_id: str, binary_sensor: str, mac: str, switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, idx, mac, name=switchbot_name) + super().__init__(coordinator, unique_id, mac, name=switchbot_name) self._sensor = binary_sensor - self._attr_unique_id = f"{idx}-{binary_sensor}" - self._attr_name = f"{switchbot_name} {binary_sensor.title()}" + self._attr_unique_id = f"{unique_id}-{binary_sensor}" self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] + self._attr_name = self.entity_description.name @property def is_on(self) -> bool: diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 362f3b01ae7..eaad573d370 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -4,39 +4,26 @@ from __future__ import annotations import logging from typing import Any -from switchbot import GetSwitchbotDevices +from switchbot import SwitchBotAdvertisement, parse_advertisement_data import voluptuous as vol +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .const import ( - CONF_RETRY_COUNT, - CONF_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT, - CONF_TIME_BETWEEN_UPDATE_COMMAND, - DEFAULT_RETRY_COUNT, - DEFAULT_RETRY_TIMEOUT, - DEFAULT_SCAN_TIMEOUT, - DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, - DOMAIN, - SUPPORTED_MODEL_TYPES, -) +from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES _LOGGER = logging.getLogger(__name__) -async def _btle_connect() -> dict: - """Scan for BTLE advertisement data.""" - - switchbot_devices = await GetSwitchbotDevices().discover() - - if not switchbot_devices: - raise NotConnectedError("Failed to discover switchbot") - - return switchbot_devices +def format_unique_id(address: str) -> str: + """Format the unique ID for a switchbot.""" + return address.replace(":", "").lower() class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): @@ -44,18 +31,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def _get_switchbots(self) -> dict: - """Try to discover nearby Switchbot devices.""" - # asyncio.lock prevents btle adapter exceptions if there are multiple calls to this method. - # store asyncio.lock in hass data if not present. - if DOMAIN not in self.hass.data: - self.hass.data.setdefault(DOMAIN, {}) - - # Discover switchbots nearby. - _btle_adv_data = await _btle_connect() - - return _btle_adv_data - @staticmethod @callback def async_get_options_flow( @@ -66,60 +41,78 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self._discovered_devices = {} + self._discovered_adv: SwitchBotAdvertisement | None = None + self._discovered_advs: dict[str, SwitchBotAdvertisement] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) + await self.async_set_unique_id(format_unique_id(discovery_info.address)) + self._abort_if_unique_id_configured() + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + if not parsed or parsed.data.get("modelName") not in SUPPORTED_MODEL_TYPES: + return self.async_abort(reason="not_supported") + self._discovered_adv = parsed + data = parsed.data + self.context["title_placeholders"] = { + "name": data["modelName"], + "address": discovery_info.address, + } + return await self.async_step_user() async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle a flow initiated by the user.""" - + """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_MAC].replace(":", "")) + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id( + format_unique_id(address), raise_on_progress=False + ) self._abort_if_unique_id_configured() - user_input[CONF_SENSOR_TYPE] = SUPPORTED_MODEL_TYPES[ - self._discovered_devices[self.unique_id]["modelName"] + self._discovered_advs[address].data["modelName"] ] - return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) - try: - self._discovered_devices = await self._get_switchbots() + if discovery := self._discovered_adv: + self._discovered_advs[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if ( + format_unique_id(address) in current_addresses + or address in self._discovered_advs + ): + continue + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + if parsed and parsed.data.get("modelName") in SUPPORTED_MODEL_TYPES: + self._discovered_advs[address] = parsed - except NotConnectedError: - return self.async_abort(reason="cannot_connect") - - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - return self.async_abort(reason="unknown") - - # Get devices already configured. - configured_devices = { - item.data[CONF_MAC] - for item in self._async_current_entries(include_ignore=False) - } - - # Get supported devices not yet configured. - unconfigured_devices = { - device["mac_address"]: f"{device['mac_address']} {device['modelName']}" - for device in self._discovered_devices.values() - if device.get("modelName") in SUPPORTED_MODEL_TYPES - and device["mac_address"] not in configured_devices - } - - if not unconfigured_devices: + if not self._discovered_advs: return self.async_abort(reason="no_unconfigured_devices") data_schema = vol.Schema( { - vol.Required(CONF_MAC): vol.In(unconfigured_devices), + vol.Required(CONF_ADDRESS): vol.In( + { + address: f"{parsed.data['modelName']} ({address})" + for address, parsed in self._discovered_advs.items() + } + ), vol.Required(CONF_NAME): str, vol.Optional(CONF_PASSWORD): str, } ) - return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) @@ -138,43 +131,15 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): """Manage Switchbot options.""" if user_input is not None: # Update common entity options for all other entities. - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id != self.config_entry.unique_id: - self.hass.config_entries.async_update_entry( - entry, options=user_input - ) return self.async_create_entry(title="", data=user_input) options = { - vol.Optional( - CONF_TIME_BETWEEN_UPDATE_COMMAND, - default=self.config_entry.options.get( - CONF_TIME_BETWEEN_UPDATE_COMMAND, - DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, - ), - ): int, vol.Optional( CONF_RETRY_COUNT, default=self.config_entry.options.get( CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT ), - ): int, - vol.Optional( - CONF_RETRY_TIMEOUT, - default=self.config_entry.options.get( - CONF_RETRY_TIMEOUT, DEFAULT_RETRY_TIMEOUT - ), - ): int, - vol.Optional( - CONF_SCAN_TIMEOUT, - default=self.config_entry.options.get( - CONF_SCAN_TIMEOUT, DEFAULT_SCAN_TIMEOUT - ), - ): int, + ): int } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) - - -class NotConnectedError(Exception): - """Exception for unable to find device.""" diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index b1587e97c10..9cc2acebbf8 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -5,21 +5,25 @@ MANUFACTURER = "switchbot" # Config Attributes ATTR_BOT = "bot" ATTR_CURTAIN = "curtain" +ATTR_HYGROMETER = "hygrometer" +ATTR_CONTACT = "contact" +ATTR_PLUG = "plug" DEFAULT_NAME = "Switchbot" -SUPPORTED_MODEL_TYPES = {"WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN} +SUPPORTED_MODEL_TYPES = { + "WoHand": ATTR_BOT, + "WoCurtain": ATTR_CURTAIN, + "WoSensorTH": ATTR_HYGROMETER, + "WoContact": ATTR_CONTACT, + "WoPlug": ATTR_PLUG, +} # Config Defaults DEFAULT_RETRY_COUNT = 3 -DEFAULT_RETRY_TIMEOUT = 5 -DEFAULT_TIME_BETWEEN_UPDATE_COMMAND = 60 -DEFAULT_SCAN_TIMEOUT = 5 # Config Options -CONF_TIME_BETWEEN_UPDATE_COMMAND = "update_time" CONF_RETRY_COUNT = "retry_count" + +# Deprecated config Entry Options to be removed in 2023.4 +CONF_TIME_BETWEEN_UPDATE_COMMAND = "update_time" CONF_RETRY_TIMEOUT = "retry_timeout" CONF_SCAN_TIMEOUT = "scan_timeout" - -# Data -DATA_COORDINATOR = "coordinator" -COMMON_OPTIONS = "common_options" diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index e8e2e240dc6..43c576249df 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -1,50 +1,74 @@ """Provides the switchbot DataUpdateCoordinator.""" from __future__ import annotations -from datetime import timedelta +import asyncio import logging +from typing import TYPE_CHECKING, Any import switchbot -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.core import HomeAssistant, callback + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice -from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): +def flatten_sensors_data(sensor): + """Deconstruct SwitchBot library temp object C/Fº readings from dictionary.""" + if "temp" in sensor["data"]: + sensor["data"]["temperature"] = sensor["data"]["temp"]["c"] + + return sensor + + +class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): """Class to manage fetching switchbot data.""" def __init__( self, hass: HomeAssistant, - *, - update_interval: int, - api: switchbot, - retry_count: int, - scan_timeout: int, + logger: logging.Logger, + ble_device: BLEDevice, + device: switchbot.SwitchbotDevice, ) -> None: """Initialize global switchbot data updater.""" - self.switchbot_api = api - self.switchbot_data = self.switchbot_api.GetSwitchbotDevices() - self.retry_count = retry_count - self.scan_timeout = scan_timeout - self.update_interval = timedelta(seconds=update_interval) - super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=self.update_interval + hass, logger, ble_device.address, bluetooth.BluetoothScanningMode.ACTIVE ) + self.ble_device = ble_device + self.device = device + self.data: dict[str, Any] = {} + self._ready_event = asyncio.Event() - async def _async_update_data(self) -> dict | None: - """Fetch data from switchbot.""" + @callback + def _async_handle_bluetooth_event( + self, + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + super()._async_handle_bluetooth_event(service_info, change) + if adv := switchbot.parse_advertisement_data( + service_info.device, service_info.advertisement + ): + self.data = flatten_sensors_data(adv.data) + if "modelName" in self.data: + self._ready_event.set() + _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) + self.device.update_from_advertisement(adv) + self.async_update_listeners() - switchbot_data = await self.switchbot_data.discover( - retry=self.retry_count, scan_timeout=self.scan_timeout - ) - - if not switchbot_data: - raise UpdateFailed("Unable to fetch switchbot services data") - - return switchbot_data + async def async_wait_ready(self) -> bool: + """Wait for the device to be ready.""" + try: + await asyncio.wait_for(self._ready_event.wait(), timeout=55) + except asyncio.TimeoutError: + return False + return True diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 9223217c173..0ae225f55d7 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -14,13 +14,12 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_RETRY_COUNT, DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -33,25 +32,17 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Switchbot curtain based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotCurtainEntity( coordinator, - entry.unique_id, - entry.data[CONF_MAC], + unique_id, + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], - coordinator.switchbot_api.SwitchbotCurtain( - mac=entry.data[CONF_MAC], - password=entry.data.get(CONF_PASSWORD), - retry_count=entry.options[CONF_RETRY_COUNT], - ), + coordinator.device, ) ] ) @@ -67,19 +58,18 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): | CoverEntityFeature.STOP | CoverEntityFeature.SET_POSITION ) - _attr_assumed_state = True def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, - mac: str, + unique_id: str, + address: str, name: str, device: SwitchbotCurtain, ) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, idx, mac, name) - self._attr_unique_id = idx + super().__init__(coordinator, unique_id, address, name) + self._attr_unique_id = unique_id self._attr_is_closed = None self._device = device @@ -90,28 +80,31 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): if not last_state or ATTR_CURRENT_POSITION not in last_state.attributes: return - self._attr_current_cover_position = last_state.attributes[ATTR_CURRENT_POSITION] - self._last_run_success = last_state.attributes["last_run_success"] - self._attr_is_closed = last_state.attributes[ATTR_CURRENT_POSITION] <= 20 + self._attr_current_cover_position = last_state.attributes.get( + ATTR_CURRENT_POSITION + ) + self._last_run_success = last_state.attributes.get("last_run_success") + if self._attr_current_cover_position is not None: + self._attr_is_closed = self._attr_current_cover_position <= 20 async def async_open_cover(self, **kwargs: Any) -> None: """Open the curtain.""" - _LOGGER.debug("Switchbot to open curtain %s", self._mac) + _LOGGER.debug("Switchbot to open curtain %s", self._address) self._last_run_success = bool(await self._device.open()) self.async_write_ha_state() async def async_close_cover(self, **kwargs: Any) -> None: """Close the curtain.""" - _LOGGER.debug("Switchbot to close the curtain %s", self._mac) + _LOGGER.debug("Switchbot to close the curtain %s", self._address) self._last_run_success = bool(await self._device.close()) self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the moving of this device.""" - _LOGGER.debug("Switchbot to stop %s", self._mac) + _LOGGER.debug("Switchbot to stop %s", self._address) self._last_run_success = bool(await self._device.stop()) self.async_write_ha_state() @@ -119,7 +112,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Move the cover shutter to a specific position.""" position = kwargs.get(ATTR_POSITION) - _LOGGER.debug("Switchbot to move at %d %s", position, self._mac) + _LOGGER.debug("Switchbot to move at %d %s", position, self._address) self._last_run_success = bool(await self._device.set_position(position)) self.async_write_ha_state() @@ -128,4 +121,5 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Handle updated data from the coordinator.""" self._attr_current_cover_position = self.data["data"]["position"] self._attr_is_closed = self.data["data"]["position"] <= 20 + self._attr_is_opening = self.data["data"]["inMotion"] self.async_write_ha_state() diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index c27c40613c7..4e69da4ec11 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -4,43 +4,58 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, +) +from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.entity import DeviceInfo from .const import MANUFACTURER from .coordinator import SwitchbotDataUpdateCoordinator -class SwitchbotEntity(CoordinatorEntity[SwitchbotDataUpdateCoordinator], Entity): +class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): """Generic entity encapsulating common features of Switchbot device.""" + coordinator: SwitchbotDataUpdateCoordinator + def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, - mac: str, + unique_id: str, + address: str, name: str, ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._last_run_success: bool | None = None - self._idx = idx - self._mac = mac + self._unique_id = unique_id + self._address = address self._attr_name = name self._attr_device_info = DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self._mac)}, + connections={(dr.CONNECTION_BLUETOOTH, self._address)}, manufacturer=MANUFACTURER, model=self.data["modelName"], name=name, ) + if ":" not in self._address: + # MacOS Bluetooth addresses are not mac addresses + return + # If the bluetooth address is also a mac address, + # add this connection as well to prevent a new device + # entry from being created when upgrading from a previous + # version of the integration. + self._attr_device_info[ATTR_CONNECTIONS].add( + (dr.CONNECTION_NETWORK_MAC, self._address) + ) @property def data(self) -> dict[str, Any]: """Return coordinator data for this entity.""" - return self.coordinator.data[self._idx] + return self.coordinator.data @property def extra_state_attributes(self) -> Mapping[Any, Any]: """Return the state attributes.""" - return {"last_run_success": self._last_run_success, "mac_address": self._mac} + return {"last_run_success": self._last_run_success} diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 23fb36a3a41..f01eae4a938 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,9 +2,24 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.14.1"], + "requirements": ["PySwitchbot==0.17.3"], "config_flow": true, - "codeowners": ["@danielhiversen", "@RenierM26"], - "iot_class": "local_polling", + "dependencies": ["bluetooth"], + "codeowners": [ + "@bdraco", + "@danielhiversen", + "@RenierM26", + "@murtas", + "@Eloston" + ], + "bluetooth": [ + { + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" + }, + { + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + } + ], + "iot_class": "local_push", "loggers": ["switchbot"] } diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 759a504d19a..fb24ae22679 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -5,20 +5,21 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_MAC, + CONF_ADDRESS, CONF_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -32,17 +33,38 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), + "wifi_rssi": SensorEntityDescription( + key="wifi_rssi", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), "battery": SensorEntityDescription( key="battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), "lightLevel": SensorEntityDescription( key="lightLevel", native_unit_of_measurement="Level", + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.ILLUMINANCE, ), + "humidity": SensorEntityDescription( + key="humidity", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + ), + "temperature": SensorEntityDescription( + key="temperature", + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), } @@ -50,23 +72,19 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Switchbot sensor based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotSensor( coordinator, - entry.unique_id, + unique_id, sensor, - entry.data[CONF_MAC], + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], ) - for sensor in coordinator.data[entry.unique_id]["data"] + for sensor in coordinator.data["data"] if sensor in SENSOR_TYPES ] ) @@ -78,16 +96,16 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, + unique_id: str, sensor: str, - mac: str, + address: str, switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, idx, mac, name=switchbot_name) + super().__init__(coordinator, unique_id, address, name=switchbot_name) self._sensor = sensor - self._attr_unique_id = f"{idx}-{sensor}" - self._attr_name = f"{switchbot_name} {sensor.title()}" + self._attr_unique_id = f"{unique_id}-{sensor}" + self._attr_name = f"{switchbot_name} {sensor.replace('_', ' ').title()}" self.entity_description = SENSOR_TYPES[sensor] @property diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 8c308083982..797d1d7613c 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -1,11 +1,11 @@ { "config": { - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "title": "Setup Switchbot device", "data": { - "mac": "Device MAC address", + "address": "Device address", "name": "[%key:common::config_flow::data::name%]", "password": "[%key:common::config_flow::data::password%]" } @@ -24,10 +24,7 @@ "step": { "init": { "data": { - "update_time": "Time between updates (seconds)", - "retry_count": "Retry count", - "retry_timeout": "Timeout between retries", - "scan_timeout": "How long to scan for advertisement data" + "retry_count": "Retry count" } } } diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 404a92eda82..65c7588acbd 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -8,13 +8,12 @@ from switchbot import Switchbot from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, STATE_ON +from homeassistant.const import CONF_ADDRESS, CONF_NAME, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_RETRY_COUNT, DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -29,46 +28,38 @@ async def async_setup_entry( async_add_entities: entity_platform.AddEntitiesCallback, ) -> None: """Set up Switchbot based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ - SwitchBotBotEntity( + SwitchBotSwitch( coordinator, - entry.unique_id, - entry.data[CONF_MAC], + unique_id, + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], - coordinator.switchbot_api.Switchbot( - mac=entry.data[CONF_MAC], - password=entry.data.get(CONF_PASSWORD), - retry_count=entry.options[CONF_RETRY_COUNT], - ), + coordinator.device, ) ] ) -class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): - """Representation of a Switchbot.""" +class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): + """Representation of a Switchbot switch.""" _attr_device_class = SwitchDeviceClass.SWITCH def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, - mac: str, + unique_id: str, + address: str, name: str, device: Switchbot, ) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, idx, mac, name) - self._attr_unique_id = idx + super().__init__(coordinator, unique_id, address, name) + self._attr_unique_id = unique_id self._device = device self._attr_is_on = False @@ -78,11 +69,11 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): if not (last_state := await self.async_get_last_state()): return self._attr_is_on = last_state.state == STATE_ON - self._last_run_success = last_state.attributes["last_run_success"] + self._last_run_success = last_state.attributes.get("last_run_success") async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" - _LOGGER.info("Turn Switchbot bot on %s", self._mac) + _LOGGER.info("Turn Switchbot bot on %s", self._address) self._last_run_success = bool(await self._device.turn_on()) if self._last_run_success: @@ -91,7 +82,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" - _LOGGER.info("Turn Switchbot bot off %s", self._mac) + _LOGGER.info("Turn Switchbot bot off %s", self._address) self._last_run_success = bool(await self._device.turn_off()) if self._last_run_success: diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index 576aeda3b16..a3ba38e2f24 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Tipus de Switchbot no compatible.", "unknown": "Error inesperat" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Adre\u00e7a del dispositiu", "mac": "Adre\u00e7a MAC del dispositiu", "name": "Nom", "password": "Contrasenya" diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index 439524c8aa6..ca306d6fa2d 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Nicht unterst\u00fctzter Switchbot-Typ.", "unknown": "Unerwarteter Fehler" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Ger\u00e4teadresse", "mac": "MAC-Adresse des Ger\u00e4ts", "name": "Name", "password": "Passwort" diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index b191c90f53c..585dd3ccbd4 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "mac": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 1cfaee8750f..b583c60061b 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Device address", "mac": "Device MAC address", "name": "Name", "password": "Password" diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index 358a4748724..a46ec376a96 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Toetamata Switchboti t\u00fc\u00fcp.", "unknown": "Ootamatu t\u00f5rge" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Seadme aadress", "mac": "Seadme MAC-aadress", "name": "Nimi", "password": "Salas\u00f5na" diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index 75eff0a9b7c..ea070f9f3c2 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Type Switchbot non pris en charge.", "unknown": "Erreur inattendue" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Adresse de l'appareil", "mac": "Adresse MAC de l'appareil", "name": "Nom", "password": "Mot de passe" diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index b870e577426..e44bfe7c811 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -15,6 +15,7 @@ "step": { "user": { "data": { + "address": "Eszk\u00f6z c\u00edme", "mac": "Eszk\u00f6z MAC-c\u00edme", "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3" diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index b8997f9247b..bcd6465acae 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -11,10 +11,11 @@ "one": "Vuoto", "other": "Vuoti" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Indirizzo del dispositivo", "mac": "Indirizzo MAC del dispositivo", "name": "Nome", "password": "Password" diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 156ddbb9924..dc43a7f610d 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -13,10 +13,11 @@ "one": "Pusty", "other": "" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Adres urz\u0105dzenia", "mac": "Adres MAC urz\u0105dzenia", "name": "Nazwa", "password": "Has\u0142o" diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 3959425cbd3..3edc04ba6f3 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Tipo de Switchbot sem suporte.", "unknown": "Erro inesperado" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Endere\u00e7o do dispositivo", "mac": "Endere\u00e7o MAC do dispositivo", "name": "Nome", "password": "Senha" diff --git a/homeassistant/components/switchbot/translations/pt.json b/homeassistant/components/switchbot/translations/pt.json new file mode 100644 index 00000000000..e2dc9fee9b4 --- /dev/null +++ b/homeassistant/components/switchbot/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_time": "Tempo entre actualiza\u00e7\u00f5es (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index 9ca076ff499..7b2f4f73cc2 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0439 \u0442\u0438\u043f Switchbot.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "mac": "MAC-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 617129167ed..6d14e05aff7 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u5225\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "\u88dd\u7f6e\u4f4d\u5740", "mac": "\u88dd\u7f6e MAC \u4f4d\u5740", "name": "\u540d\u7a31", "password": "\u5bc6\u78bc" diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index d4779e8e748..31273dce23d 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -1,7 +1,6 @@ """The Switcher integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -96,26 +95,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] = SwitcherDataUpdateCoordinator(hass, entry, device) coordinator.async_setup() - async def platforms_setup_task() -> None: - # Must be ready before dispatcher is called - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) + # Must be ready before dispatcher is called + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - discovery_task = hass.data[DOMAIN].pop(DATA_DISCOVERY, None) - if discovery_task is not None: - discovered_devices = await discovery_task - for device in discovered_devices.values(): - on_device_data_callback(device) + discovery_task = hass.data[DOMAIN].pop(DATA_DISCOVERY, None) + if discovery_task is not None: + discovered_devices = await discovery_task + for device in discovered_devices.values(): + on_device_data_callback(device) - await async_start_bridge(hass, on_device_data_callback) + await async_start_bridge(hass, on_device_data_callback) - hass.async_create_task(platforms_setup_task()) - - @callback async def stop_bridge(event: Event) -> None: await async_stop_bridge(hass) diff --git a/homeassistant/components/switcher_kis/translations/ja.json b/homeassistant/components/switcher_kis/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/switcher_kis/translations/ja.json +++ b/homeassistant/components/switcher_kis/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/switchmate/manifest.json b/homeassistant/components/switchmate/manifest.json index c4a263aca19..930fb6bf88e 100644 --- a/homeassistant/components/switchmate/manifest.json +++ b/homeassistant/components/switchmate/manifest.json @@ -2,8 +2,8 @@ "domain": "switchmate", "name": "Switchmate SimplySmart Home", "documentation": "https://www.home-assistant.io/integrations/switchmate", - "requirements": ["pySwitchmate==0.4.6"], - "codeowners": ["@danielhiversen"], + "requirements": ["pySwitchmate==0.5.1"], + "codeowners": ["@danielhiversen", "@qiz-li"], "iot_class": "local_polling", "loggers": ["switchmate"] } diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index b0f2b58a1fa..7beb89f8de1 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta # pylint: disable=import-error -import switchmate +from switchmate import Switchmate import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -49,7 +49,7 @@ class SwitchmateEntity(SwitchEntity): self._mac = mac self._name = name - self._device = switchmate.Switchmate(mac=mac, flip_on_off=flip_on_off) + self._device = Switchmate(mac=mac, flip_on_off=flip_on_off) @property def unique_id(self) -> str: @@ -66,19 +66,19 @@ class SwitchmateEntity(SwitchEntity): """Return the name of the switch.""" return self._name - def update(self) -> None: + async def async_update(self) -> None: """Synchronize state with switch.""" - self._device.update() + await self._device.update() @property def is_on(self) -> bool: """Return true if it is on.""" return self._device.state - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the switch on.""" - self._device.turn_on() + await self._device.turn_on() - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn the switch off.""" - self._device.turn_off() + await self._device.turn_off() diff --git a/homeassistant/components/syncthing/__init__.py b/homeassistant/components/syncthing/__init__.py index ac9d5d32e92..3950f908e85 100644 --- a/homeassistant/components/syncthing/__init__.py +++ b/homeassistant/components/syncthing/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: syncthing.subscribe() hass.data[DOMAIN][entry.entry_id] = syncthing - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def cancel_listen_task(_): await syncthing.unsubscribe() diff --git a/homeassistant/components/syncthing/translations/cs.json b/homeassistant/components/syncthing/translations/cs.json index a679dc35fe3..df806ffaf1c 100644 --- a/homeassistant/components/syncthing/translations/cs.json +++ b/homeassistant/components/syncthing/translations/cs.json @@ -4,6 +4,7 @@ "already_configured": "Slu\u017eba je ji\u017e nastavena" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" } } diff --git a/homeassistant/components/syncthing/translations/ja.json b/homeassistant/components/syncthing/translations/ja.json index 2a725cbf3cc..ee9c4b02e26 100644 --- a/homeassistant/components/syncthing/translations/ja.json +++ b/homeassistant/components/syncthing/translations/ja.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "title": "Syncthing\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", + "title": "Syncthing\u7d71\u5408\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", "token": "\u30c8\u30fc\u30af\u30f3", "url": "URL", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" diff --git a/homeassistant/components/syncthing/translations/pt.json b/homeassistant/components/syncthing/translations/pt.json new file mode 100644 index 00000000000..51baddfeab3 --- /dev/null +++ b/homeassistant/components/syncthing/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 792267791eb..988c92de593 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=printer.hostname(), ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 4536e703ce9..f0ffe081faf 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,7 +3,7 @@ "name": "Samsung SyncThru Printer", "documentation": "https://www.home-assistant.io/integrations/syncthru", "config_flow": true, - "requirements": ["pysyncthru==0.7.10", "url-normalize==1.4.1"], + "requirements": ["pysyncthru==0.7.10", "url-normalize==1.4.3"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:Printer:1", diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index e6868491eae..c37540b14b7 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -121,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator_switches=coordinator_switches, ) hass.data.setdefault(DOMAIN, {})[entry.unique_id] = synology_data - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 89bbc4ae8c2..0314165eb41 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -383,7 +383,7 @@ class SynologyDSMOptionsFlowHandler(OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) -def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str) -> str: +def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) -> str: """Login to the NAS and fetch basic data.""" # These do i/o api.login(otp_code) diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 47245b2ceb8..2654c5f3910 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -35,7 +35,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "title": "Synology DSM \u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "Synology DSM \u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/pt.json b/homeassistant/components/synology_dsm/translations/pt.json index 9745f897e05..66df18026ea 100644 --- a/homeassistant/components/synology_dsm/translations/pt.json +++ b/homeassistant/components/synology_dsm/translations/pt.json @@ -23,6 +23,12 @@ "verify_ssl": "Verificar o certificado SSL" } }, + "reauth_confirm": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 1bee974a4c4..19bcc224a66 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -123,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL): return True diff --git a/homeassistant/components/system_bridge/translations/hu.json b/homeassistant/components/system_bridge/translations/hu.json index c570cd2e3c2..0671bcce90b 100644 --- a/homeassistant/components/system_bridge/translations/hu.json +++ b/homeassistant/components/system_bridge/translations/hu.json @@ -24,7 +24,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg kapcsolati adatait." + "description": "K\u00e9rem, adja meg kapcsolati adatait." } } } diff --git a/homeassistant/components/system_bridge/translations/pt.json b/homeassistant/components/system_bridge/translations/pt.json new file mode 100644 index 00000000000..8f319572c97 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index be7232245e1..4821537fc8b 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable import dataclasses from datetime import datetime import logging +from typing import Any import aiohttp import async_timeout @@ -29,7 +30,7 @@ INFO_CALLBACK_TIMEOUT = 5 def async_register_info( hass: HomeAssistant, domain: str, - info_callback: Callable[[HomeAssistant], dict], + info_callback: Callable[[HomeAssistant], Awaitable[dict]], ): """Register an info callback. @@ -61,9 +62,10 @@ async def _register_system_health_platform(hass, integration_domain, platform): async def get_integration_info( hass: HomeAssistant, registration: SystemHealthRegistration -): +) -> dict[str, Any]: """Get integration system health.""" try: + assert registration.info_callback async with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): data = await registration.info_callback(hass) except asyncio.TimeoutError: @@ -72,7 +74,7 @@ async def get_integration_info( _LOGGER.exception("Error fetching info") data = {"error": {"type": "failed", "error": "unknown"}} - result = {"info": data} + result: dict[str, Any] = {"info": data} if registration.manage_url: result["manage_url"] = registration.manage_url @@ -88,15 +90,15 @@ def _format_value(val): return val -@websocket_api.async_response @websocket_api.websocket_command({vol.Required("type"): "system_health/info"}) +@websocket_api.async_response async def handle_info( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Handle an info request via a subscription.""" registrations: dict[str, SystemHealthRegistration] = hass.data[DOMAIN] data = {} - pending_info = {} + pending_info: dict[tuple[str, str], asyncio.Task] = {} for domain, domain_data in zip( registrations, @@ -138,7 +140,10 @@ async def handle_info( ) return - tasks = [asyncio.create_task(stop_event.wait()), *pending_info.values()] + tasks: set[asyncio.Task] = { + asyncio.create_task(stop_event.wait()), + *pending_info.values(), + } pending_lookup = {val: key for key, val in pending_info.items()} # One task is the stop_event.wait() and is always there @@ -160,8 +165,7 @@ async def handle_info( "key": key, } - if result.exception(): - exception = result.exception() + if exception := result.exception(): _LOGGER.error( "Error fetching system info for %s - %s", domain, @@ -206,7 +210,7 @@ class SystemHealthRegistration: async def async_check_can_reach_url( hass: HomeAssistant, url: str, more_info: str | None = None -) -> str: +) -> str | dict[str, str]: """Test if the url can be reached.""" session = aiohttp_client.async_get_clientsession(hass) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 343830fe690..b0d538a4ff8 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -1,7 +1,6 @@ """Support for system log.""" from collections import OrderedDict, deque import logging -import queue import re import traceback @@ -9,7 +8,7 @@ import voluptuous as vol from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components import websocket_api -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -56,8 +55,7 @@ SERVICE_WRITE_SCHEMA = vol.Schema( ) -def _figure_out_source(record, call_stack, hass): - paths = [HOMEASSISTANT_PATH[0], hass.config.config_dir] +def _figure_out_source(record, call_stack, paths_re): # If a stack trace exists, extract file names from the entire call stack. # The other case is when a regular "log" is made (without an attached @@ -78,11 +76,10 @@ def _figure_out_source(record, call_stack, hass): # Iterate through the stack call (in reverse) and find the last call from # a file in Home Assistant. Try to figure out where error happened. - paths_re = r"(?:{})/(.*)".format("|".join([re.escape(x) for x in paths])) for pathname in reversed(stack): # Try to match with a file within Home Assistant - if match := re.match(paths_re, pathname[0]): + if match := paths_re.match(pathname[0]): return [match.group(1), pathname[1]] # Ok, we don't know what this is return (record.pathname, record.lineno) @@ -157,26 +154,16 @@ class DedupStore(OrderedDict): return [value.to_dict() for value in reversed(self.values())] -class LogErrorQueueHandler(logging.handlers.QueueHandler): - """Process the log in another thread.""" - - def emit(self, record): - """Emit a log record.""" - try: - self.enqueue(record) - except Exception: # pylint: disable=broad-except - self.handleError(record) - - class LogErrorHandler(logging.Handler): """Log handler for error messages.""" - def __init__(self, hass, maxlen, fire_event): + def __init__(self, hass, maxlen, fire_event, paths_re): """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass self.records = DedupStore(maxlen=maxlen) self.fire_event = fire_event + self.paths_re = paths_re def emit(self, record): """Save error and warning logs. @@ -189,7 +176,9 @@ class LogErrorHandler(logging.Handler): if not record.exc_info: stack = [(f[0], f[1]) for f in traceback.extract_stack()] - entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass)) + entry = LogEntry( + record, stack, _figure_out_source(record, stack, self.paths_re) + ) self.records.add_entry(entry) if self.fire_event: self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict()) @@ -200,29 +189,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if (conf := config.get(DOMAIN)) is None: conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - simple_queue: queue.SimpleQueue = queue.SimpleQueue() - queue_handler = LogErrorQueueHandler(simple_queue) - queue_handler.setLevel(logging.WARN) - logging.root.addHandler(queue_handler) - - handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT]) + hass_path: str = HOMEASSISTANT_PATH[0] + config_dir = hass.config.config_dir + assert config_dir is not None + paths_re = re.compile( + r"(?:{})/(.*)".format("|".join([re.escape(x) for x in (hass_path, config_dir)])) + ) + handler = LogErrorHandler( + hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT], paths_re + ) + handler.setLevel(logging.WARN) hass.data[DOMAIN] = handler - listener = logging.handlers.QueueListener( - simple_queue, handler, respect_handler_level=True - ) - - listener.start() - @callback - def _async_stop_queue_handler(_) -> None: + def _async_stop_handler(_) -> None: """Cleanup handler.""" - logging.root.removeHandler(queue_handler) - listener.stop() + logging.root.removeHandler(handler) del hass.data[DOMAIN] - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_queue_handler) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_handler) + + logging.root.addHandler(handler) websocket_api.async_register_command(hass, list_errors) @@ -238,13 +226,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: level = service.data[CONF_LEVEL] getattr(logger, level)(service.data[CONF_MESSAGE]) - async def async_shutdown_handler(event): - """Remove logging handler when Home Assistant is shutdown.""" - # This is needed as older logger instances will remain - logging.getLogger().removeHandler(handler) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown_handler) - hass.services.async_register( DOMAIN, SERVICE_CLEAR, async_service_handler, schema=SERVICE_CLEAR_SCHEMA ) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index b9fe86cbfa8..398614815a2 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -2,7 +2,7 @@ "domain": "systemmonitor", "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": ["psutil==5.9.0"], + "requirements": ["psutil==5.9.1"], "codeowners": [], "iot_class": "local_push", "loggers": ["psutil"] diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 0029dbf8c89..7417ac49c96 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UPDATE_LISTENER: update_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py index 7388eec22df..abc4c4ca399 100644 --- a/homeassistant/components/tailscale/__init__.py +++ b/homeassistant/components/tailscale/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -41,6 +41,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class TailscaleEntity(CoordinatorEntity): """Defines a Tailscale base entity.""" + _attr_has_entity_name = True + def __init__( self, *, @@ -52,8 +54,6 @@ class TailscaleEntity(CoordinatorEntity): super().__init__(coordinator=coordinator) self.entity_description = description self.device_id = device.device_id - self.friendly_name = device.name.split(".")[0] - self._attr_name = f"{self.friendly_name} {description.name}" self._attr_unique_id = f"{device.device_id}_{description.key}" @property @@ -71,6 +71,6 @@ class TailscaleEntity(CoordinatorEntity): identifiers={(DOMAIN, device.device_id)}, manufacturer="Tailscale Inc.", model=device.os, - name=self.friendly_name, + name=device.name.split(".")[0], sw_version=device.client_version, ) diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py index 2f97d307b15..94176916dec 100644 --- a/homeassistant/components/tailscale/binary_sensor.py +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -44,7 +44,7 @@ BINARY_SENSORS: tuple[TailscaleBinarySensorEntityDescription, ...] = ( ), TailscaleBinarySensorEntityDescription( key="client_supports_hair_pinning", - name="Supports Hairpinning", + name="Supports hairpinning", icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.hair_pinning, diff --git a/homeassistant/components/tailscale/sensor.py b/homeassistant/components/tailscale/sensor.py index 07f7dbe91cc..13d8a6db0cf 100644 --- a/homeassistant/components/tailscale/sensor.py +++ b/homeassistant/components/tailscale/sensor.py @@ -45,14 +45,14 @@ SENSORS: tuple[TailscaleSensorEntityDescription, ...] = ( ), TailscaleSensorEntityDescription( key="ip", - name="IP Address", + name="IP address", icon="mdi:ip-network", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.addresses[0] if device.addresses else None, ), TailscaleSensorEntityDescription( key="last_seen", - name="Last Seen", + name="Last seen", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.last_seen, ), diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index b7a15e82ea8..3db67b4c8be 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -136,7 +136,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tankerkoenig/translations/pt.json b/homeassistant/components/tankerkoenig/translations/pt.json new file mode 100644 index 00000000000..7af02efc468 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "name": "Nome da regi\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/ru.json b/homeassistant/components/tankerkoenig/translations/ru.json index bf61fddc0c5..d2b34eb264b 100644 --- a/homeassistant/components/tankerkoenig/translations/ru.json +++ b/homeassistant/components/tankerkoenig/translations/ru.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "no_stations": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u043d\u0438 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u0432 \u0440\u0430\u0434\u0438\u0443\u0441\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "select_station": { "data": { "stations": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438" @@ -32,7 +38,8 @@ "init": { "data": { "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f", - "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", + "stations": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Tankerkoenig" } diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 7e37c6ea7f2..da9d7e0d53b 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -1,7 +1,6 @@ """The Tasmota integration.""" from __future__ import annotations -import asyncio import logging from hatasmota.const import ( @@ -75,21 +74,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, mac, config, entry, tasmota_mqtt, device_registry ) - async def start_platforms() -> None: - await device_automation.async_setup_entry(hass, entry) - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) + await device_automation.async_setup_entry(hass, entry) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX] + await discovery.async_start( + hass, discovery_prefix, entry, tasmota_mqtt, async_discover_device + ) - discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX] - await discovery.async_start( - hass, discovery_prefix, entry, tasmota_mqtt, async_discover_device - ) - - hass.async_create_task(start_platforms()) return True diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index e2109a16afe..d8981090d58 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -6,8 +6,9 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.mqtt import MqttServiceInfo, valid_subscribe_topic +from homeassistant.components.mqtt import valid_subscribe_topic from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN diff --git a/homeassistant/components/tasmota/translations/ja.json b/homeassistant/components/tasmota/translations/ja.json index 8aad41de32f..34c56d4d413 100644 --- a/homeassistant/components/tasmota/translations/ja.json +++ b/homeassistant/components/tasmota/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_discovery_topic": "(\u4e0d\u6b63\u306a)Invalid discovery topic prefix." diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index 339ec6eb895..bd5032014bc 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = TautulliDataUpdateCoordinator(hass, host_configuration, api_client) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -46,6 +46,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class TautulliEntity(CoordinatorEntity[TautulliDataUpdateCoordinator]): """Defines a base Tautulli entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: TautulliDataUpdateCoordinator, diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 1d5efde7cc7..5653aa5dc57 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -60,14 +60,14 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( icon="mdi:plex", key="watching_count", - name="Tautulli", + name="Watching", native_unit_of_measurement="Watching", value_fn=lambda home_stats, activity, _: cast(int, activity.stream_count), ), TautulliSensorEntityDescription( icon="mdi:plex", key="stream_count_direct_play", - name="Direct Plays", + name="Direct plays", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="Streams", entity_registry_enabled_default=False, @@ -78,7 +78,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( icon="mdi:plex", key="stream_count_direct_stream", - name="Direct Streams", + name="Direct streams", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="Streams", entity_registry_enabled_default=False, @@ -99,7 +99,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), TautulliSensorEntityDescription( key="total_bandwidth", - name="Total Bandwidth", + name="Total bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DATA_KILOBITS, state_class=SensorStateClass.MEASUREMENT, @@ -107,7 +107,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), TautulliSensorEntityDescription( key="lan_bandwidth", - name="LAN Bandwidth", + name="LAN bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DATA_KILOBITS, entity_registry_enabled_default=False, @@ -116,7 +116,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), TautulliSensorEntityDescription( key="wan_bandwidth", - name="WAN Bandwidth", + name="WAN bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DATA_KILOBITS, entity_registry_enabled_default=False, @@ -126,21 +126,21 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( icon="mdi:movie-open", key="top_movies", - name="Top Movie", + name="Top movie", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( icon="mdi:television", key="top_tv", - name="Top TV Show", + name="Top TV show", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( icon="mdi:walk", key=ATTR_TOP_USER, - name="Top User", + name="Top user", entity_registry_enabled_default=False, value_fn=get_top_stats, ), @@ -170,7 +170,7 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( ), TautulliSessionSensorEntityDescription( key="full_title", - name="Full Title", + name="Full title", entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.full_title), ), @@ -184,7 +184,7 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( ), TautulliSessionSensorEntityDescription( key="stream_resolution", - name="Stream Resolution", + name="Stream resolution", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.stream_video_resolution), @@ -192,21 +192,21 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( TautulliSessionSensorEntityDescription( icon="mdi:plex", key="transcode_decision", - name="Transcode Decision", + name="Transcode decision", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.transcode_decision), ), TautulliSessionSensorEntityDescription( key="session_thumb", - name="session Thumbnail", + name="session thumbnail", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.user_thumb), ), TautulliSessionSensorEntityDescription( key="video_resolution", - name="Video Resolution", + name="Video resolution", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.video_resolution), @@ -284,7 +284,6 @@ class TautulliSessionSensor(TautulliEntity, SensorEntity): super().__init__(coordinator, description, user) entry_id = coordinator.config_entry.entry_id self._attr_unique_id = f"{entry_id}_{user.user_id}_{description.key}" - self._attr_name = f"{user.username} {description.name}" @property def native_value(self) -> StateType: diff --git a/homeassistant/components/tautulli/translations/ja.json b/homeassistant/components/tautulli/translations/ja.json index 6f733e1cad4..2407bb4b984 100644 --- a/homeassistant/components/tautulli/translations/ja.json +++ b/homeassistant/components/tautulli/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/tautulli/translations/pt.json b/homeassistant/components/tautulli/translations/pt.json new file mode 100644 index 00000000000..43d522f0ab1 --- /dev/null +++ b/homeassistant/components/tautulli/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index be1c8325c5f..f4738334745 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -19,6 +19,7 @@ from telegram import ( ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, + User, ) from telegram.error import TelegramError from telegram.ext import CallbackContext, Filters @@ -966,16 +967,17 @@ class BaseTelegramBotEntity: event_data[ATTR_TEXT] = message.text if message.from_user: - event_data.update( - { - ATTR_USER_ID: message.from_user.id, - ATTR_FROM_FIRST: message.from_user.first_name, - ATTR_FROM_LAST: message.from_user.last_name, - } - ) + event_data.update(self._get_user_event_data(message.from_user)) return event_type, event_data + def _get_user_event_data(self, user: User) -> dict[str, Any]: + return { + ATTR_USER_ID: user.id, + ATTR_FROM_FIRST: user.first_name, + ATTR_FROM_LAST: user.last_name, + } + def _get_callback_query_event_data( self, callback_query: CallbackQuery ) -> tuple[str, dict[str, Any]]: @@ -991,6 +993,9 @@ class BaseTelegramBotEntity: event_data[ATTR_MSG] = callback_query.message.to_dict() event_data[ATTR_CHAT_ID] = callback_query.message.chat.id + if callback_query.from_user: + event_data.update(self._get_user_event_data(callback_query.from_user)) + # Split data into command and args if possible event_data.update(self._get_command_event_data(callback_query.data)) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 4c0de6ade1e..762ba08ab26 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -22,10 +22,11 @@ async def async_setup_platform(hass, bot, config): return True -def process_error(update: Update, context: CallbackContext): +def process_error(update: Update, context: CallbackContext) -> None: """Telegram bot error handler.""" try: - raise context.error + if context.error: + raise context.error except (TimedOut, NetworkError, RetryAfter): # Long polling timeout or connection problem. Nothing serious. pass diff --git a/homeassistant/components/tellduslive/translations/pt.json b/homeassistant/components/tellduslive/translations/pt.json index 1f06d33d356..e8fda8a7647 100644 --- a/homeassistant/components/tellduslive/translations/pt.json +++ b/homeassistant/components/tellduslive/translations/pt.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", - "unknown": "Ocorreu um erro desconhecido", + "unknown": "Erro inesperado", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "error": { diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index ae26e58ac04..d7c117c9be7 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -19,8 +19,10 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -41,8 +43,10 @@ from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_con _LOGGER = logging.getLogger(__name__) _VALID_STATES = [ STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -51,9 +55,12 @@ _VALID_STATES = [ ] CONF_ARM_AWAY_ACTION = "arm_away" +CONF_ARM_CUSTOM_BYPASS_ACTION = "arm_custom_bypass" CONF_ARM_HOME_ACTION = "arm_home" CONF_ARM_NIGHT_ACTION = "arm_night" +CONF_ARM_VACATION_ACTION = "arm_vacation" CONF_DISARM_ACTION = "disarm" +CONF_TRIGGER_ACTION = "trigger" CONF_ALARM_CONTROL_PANELS = "panels" CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_FORMAT = "code_format" @@ -72,8 +79,11 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum( TemplateCodeFormat @@ -157,6 +167,19 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): self._arm_night_script = None if (arm_night_action := config.get(CONF_ARM_NIGHT_ACTION)) is not None: self._arm_night_script = Script(hass, arm_night_action, name, DOMAIN) + self._arm_vacation_script = None + if (arm_vacation_action := config.get(CONF_ARM_VACATION_ACTION)) is not None: + self._arm_vacation_script = Script(hass, arm_vacation_action, name, DOMAIN) + self._arm_custom_bypass_script = None + if ( + arm_custom_bypass_action := config.get(CONF_ARM_CUSTOM_BYPASS_ACTION) + ) is not None: + self._arm_custom_bypass_script = Script( + hass, arm_custom_bypass_action, name, DOMAIN + ) + self._trigger_script = None + if (trigger_action := config.get(CONF_TRIGGER_ACTION)) is not None: + self._trigger_script = Script(hass, trigger_action, name, DOMAIN) self._state: str | None = None @@ -184,6 +207,21 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): supported_features | AlarmControlPanelEntityFeature.ARM_AWAY ) + if self._arm_vacation_script is not None: + supported_features = ( + supported_features | AlarmControlPanelEntityFeature.ARM_VACATION + ) + + if self._arm_custom_bypass_script is not None: + supported_features = ( + supported_features | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + ) + + if self._trigger_script is not None: + supported_features = ( + supported_features | AlarmControlPanelEntityFeature.TRIGGER + ) + return supported_features @property @@ -257,8 +295,28 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): STATE_ALARM_ARMED_NIGHT, script=self._arm_night_script, code=code ) + async def async_alarm_arm_vacation(self, code: str | None = None) -> None: + """Arm the panel to Vacation.""" + await self._async_alarm_arm( + STATE_ALARM_ARMED_VACATION, script=self._arm_vacation_script, code=code + ) + + async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: + """Arm the panel to Custom Bypass.""" + await self._async_alarm_arm( + STATE_ALARM_ARMED_CUSTOM_BYPASS, + script=self._arm_custom_bypass_script, + code=code, + ) + async def async_alarm_disarm(self, code: str | None = None) -> None: """Disarm the panel.""" await self._async_alarm_arm( STATE_ALARM_DISARMED, script=self._disarm_script, code=code ) + + async def async_alarm_trigger(self, code: str | None = None) -> None: + """Trigger the panel.""" + await self._async_alarm_arm( + STATE_ALARM_TRIGGERED, script=self._trigger_script, code=code + ) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index ab7c88e8b8c..bad4a7d7059 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ( @@ -208,16 +209,18 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): ENTITY_ID_FORMAT, object_id, hass=hass ) - self._device_class = config.get(CONF_DEVICE_CLASS) + self._device_class: BinarySensorDeviceClass | None = config.get( + CONF_DEVICE_CLASS + ) self._template = config[CONF_STATE] - self._state = None + self._state: bool | None = None self._delay_cancel = None self._delay_on = None self._delay_on_raw = config.get(CONF_DELAY_ON) self._delay_off = None self._delay_off_raw = config.get(CONF_DELAY_OFF) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore state and register callbacks.""" if ( (self._delay_on_raw is not None or self._delay_off_raw is not None) @@ -283,12 +286,12 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): self._delay_cancel = async_call_later(self.hass, delay, _set_state) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._state @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass | None: """Return the sensor class of the binary sensor.""" return self._device_class diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 807e3e79ef8..f4ad971c1d6 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -197,17 +198,17 @@ class LightTemplate(TemplateEntity, LightEntity): self._supports_transition = False @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of the light.""" return self._brightness @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the CT color value in mireds.""" return self._temperature @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the max mireds value in mireds.""" if self._max_mireds is not None: return self._max_mireds @@ -215,7 +216,7 @@ class LightTemplate(TemplateEntity, LightEntity): return super().max_mireds @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the min mireds value in mireds.""" if self._min_mireds is not None: return self._min_mireds @@ -223,27 +224,27 @@ class LightTemplate(TemplateEntity, LightEntity): return super().min_mireds @property - def white_value(self): + def white_value(self) -> int | None: """Return the white value.""" return self._white_value @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" return self._color @property - def effect(self): + def effect(self) -> str | None: """Return the effect.""" return self._effect @property - def effect_list(self): + def effect_list(self) -> list[str] | None: """Return the effect list.""" return self._effect_list @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = 0 if self._level_script is not None: @@ -261,11 +262,11 @@ class LightTemplate(TemplateEntity, LightEntity): return supported_features @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template: self.add_template_attribute( @@ -345,7 +346,7 @@ class LightTemplate(TemplateEntity, LightEntity): ) await super().async_added_to_hass() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" optimistic_set = False # set optimistic states @@ -448,7 +449,7 @@ class LightTemplate(TemplateEntity, LightEntity): if optimistic_set: self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if ATTR_TRANSITION in kwargs and self._supports_transition is True: await self.async_run_script( diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index d41bdee597b..89f4d22b957 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -14,6 +14,7 @@ from homeassistant.components.number.const import ( ATTR_VALUE, DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, + DEFAULT_STEP, DOMAIN as NUMBER_DOMAIN, ) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID @@ -119,10 +120,9 @@ class TemplateNumber(TemplateEntity, NumberEntity): self._min_value_template = config[ATTR_MIN] self._max_value_template = config[ATTR_MAX] self._attr_assumed_state = self._optimistic = config[CONF_OPTIMISTIC] - self._attr_native_value = None - self._attr_native_step = None - self._attr_native_min_value = None - self._attr_native_max_value = None + self._attr_native_step = DEFAULT_STEP + self._attr_native_min_value = DEFAULT_MIN_VALUE + self._attr_native_max_value = DEFAULT_MAX_VALUE async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index ee1ddfa8c2d..b52953c1a8a 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -205,13 +205,13 @@ class SensorTemplate(TemplateSensor): ) -> None: """Initialize the sensor.""" super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) - self._template = config.get(CONF_STATE) + self._template: template.Template = config[CONF_STATE] if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.add_template_attribute( "_attr_native_value", self._template, None, self._update_state diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index f04f2b5ba7a..9f282cb9b11 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -1,6 +1,8 @@ """Support for switches which integrates with other components.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.switch import ( @@ -110,7 +112,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self._template = config.get(CONF_VALUE_TEMPLATE) self._on_script = Script(hass, config[ON_ACTION], friendly_name, DOMAIN) self._off_script = Script(hass, config[OFF_ACTION], friendly_name, DOMAIN) - self._state = False + self._state: bool | None = False @callback def _update_state(self, result): @@ -129,7 +131,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self._state = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template is None: @@ -147,18 +149,18 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): await super().async_added_to_hass() @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Fire the on action.""" await self.async_run_script(self._on_script, context=self._context) if self._template is None: self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Fire the off action.""" await self.async_run_script(self._off_script, context=self._context) if self._template is None: @@ -166,6 +168,6 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self.async_write_ha_state() @property - def assumed_state(self): + def assumed_state(self) -> bool: """State is assumed, if no template given.""" return self._template is None diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 5f306bfa5e1..0a74ee5c5fc 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -200,7 +200,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] @property - def state(self): + def state(self) -> str | None: """Return the status of the vacuum cleaner.""" return self._state @@ -263,7 +263,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template is not None: self.add_template_attribute( diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 42d0eae1ecd..c8a62791429 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,8 +6,8 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.23.0", - "pillow==9.1.1" + "numpy==1.23.1", + "pillow==9.2.0" ], "codeowners": [], "iot_class": "local_polling", diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index c4c7c551375..b1d5811c610 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_coordinator=coordinator, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/tesla_wall_connector/translations/pt.json b/homeassistant/components/tesla_wall_connector/translations/pt.json new file mode 100644 index 00000000000..8578f969852 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/__init__.py b/homeassistant/components/threshold/__init__.py index 75f4c5d1abc..2ca1410a890 100644 --- a/homeassistant/components/threshold/__init__.py +++ b/homeassistant/components/threshold/__init__.py @@ -7,7 +7,9 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Min/Max from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + await hass.config_entries.async_forward_entry_setups( + entry, (Platform.BINARY_SENSOR,) + ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/threshold/translations/sv.json b/homeassistant/components/threshold/translations/sv.json index 613b2c25412..dd247031a4e 100644 --- a/homeassistant/components/threshold/translations/sv.json +++ b/homeassistant/components/threshold/translations/sv.json @@ -2,6 +2,11 @@ "config": { "error": { "need_lower_upper": "Undre och \u00f6vre gr\u00e4ns kan inte vara tomma" + }, + "step": { + "user": { + "title": "L\u00e4gg till gr\u00e4nsv\u00e4rdessensor" + } } }, "options": { @@ -12,5 +17,6 @@ } } } - } + }, + "title": "Gr\u00e4nsv\u00e4rdessensor" } \ No newline at end of file diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index a0fda8823c4..34f5843412e 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Failed to login. %s", exp) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 0787e00a489..38f52a067fa 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -1,6 +1,7 @@ """The Tile component.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from functools import partial @@ -17,7 +18,7 @@ from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_e from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.async_ import gather_with_concurrency -from .const import DATA_COORDINATOR, DATA_TILE, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER PLATFORMS = [Platform.DEVICE_TRACKER] DEVICE_TYPES = ["PHONE", "TILE"] @@ -28,6 +29,14 @@ DEFAULT_UPDATE_INTERVAL = timedelta(minutes=2) CONF_SHOW_INACTIVE = "show_inactive" +@dataclass +class TileData: + """Define an object to be stored in `hass.data`.""" + + coordinators: dict[str, DataUpdateCoordinator] + tiles: dict[str, Tile] + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tile as config entry.""" @@ -100,12 +109,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_COORDINATOR: coordinators, - DATA_TILE: tiles, - } + hass.data[DOMAIN][entry.entry_id] = TileData(coordinators=coordinators, tiles=tiles) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tile/const.py b/homeassistant/components/tile/const.py index 0f6f0dabb5c..eed3eb698ef 100644 --- a/homeassistant/components/tile/const.py +++ b/homeassistant/components/tile/const.py @@ -3,7 +3,4 @@ import logging DOMAIN = "tile" -DATA_COORDINATOR = "coordinator" -DATA_TILE = "tile" - LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index df5f035a849..61e9a1bdcd9 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -18,7 +18,8 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DATA_COORDINATOR, DATA_TILE, DOMAIN +from . import TileData +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -38,14 +39,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Tile device trackers.""" + data: TileData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( [ - TileDeviceTracker( - entry, - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][tile_uuid], - tile, - ) - for tile_uuid, tile in hass.data[DOMAIN][entry.entry_id][DATA_TILE].items() + TileDeviceTracker(entry, data.coordinators[tile_uuid], tile) + for tile_uuid, tile in data.tiles.items() ] ) diff --git a/homeassistant/components/tile/diagnostics.py b/homeassistant/components/tile/diagnostics.py index 7e85ef25047..8654bf44680 100644 --- a/homeassistant/components/tile/diagnostics.py +++ b/homeassistant/components/tile/diagnostics.py @@ -3,14 +3,13 @@ from __future__ import annotations from typing import Any -from pytile.tile import Tile - from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from .const import DATA_TILE, DOMAIN +from . import TileData +from .const import DOMAIN CONF_ALTITUDE = "altitude" CONF_UUID = "uuid" @@ -27,8 +26,8 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - tiles: dict[str, Tile] = hass.data[DOMAIN][entry.entry_id][DATA_TILE] + data: TileData = hass.data[DOMAIN][entry.entry_id] return async_redact_data( - {"tiles": [tile.as_dict() for tile in tiles.values()]}, TO_REDACT + {"tiles": [tile.as_dict() for tile in data.tiles.values()]}, TO_REDACT ) diff --git a/homeassistant/components/tile/translations/pt.json b/homeassistant/components/tile/translations/pt.json index bfafaa77b42..e883bd875e1 100644 --- a/homeassistant/components/tile/translations/pt.json +++ b/homeassistant/components/tile/translations/pt.json @@ -7,10 +7,15 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", - "username": "E-mail" + "username": "Email" }, "title": "Configurar Tile" } diff --git a/homeassistant/components/timer/translations/pt.json b/homeassistant/components/timer/translations/pt.json index a49163aed8c..1f506228973 100644 --- a/homeassistant/components/timer/translations/pt.json +++ b/homeassistant/components/timer/translations/pt.json @@ -1,7 +1,7 @@ { "state": { "_": { - "active": "ativo", + "active": "Ativo", "idle": "Em espera", "paused": "Em pausa" } diff --git a/homeassistant/components/tod/__init__.py b/homeassistant/components/tod/__init__.py index 09038836e2e..e404826534e 100644 --- a/homeassistant/components/tod/__init__.py +++ b/homeassistant/components/tod/__init__.py @@ -8,7 +8,9 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Times of the Day from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + await hass.config_entries.async_forward_entry_setups( + entry, (Platform.BINARY_SENSOR,) + ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/tod/translations/pt.json b/homeassistant/components/tod/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/tod/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 3b78adacbac..a75cb7ce298 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tomorrowio/translations/hu.json b/homeassistant/components/tomorrowio/translations/hu.json index d619b6346a4..4d906cd1e03 100644 --- a/homeassistant/components/tomorrowio/translations/hu.json +++ b/homeassistant/components/tomorrowio/translations/hu.json @@ -3,7 +3,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", - "rate_limited": "Jelenleg korl\u00e1tozott a hozz\u00e1f\u00e9r\u00e9s, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "rate_limited": "Jelenleg korl\u00e1tozott a hozz\u00e1f\u00e9r\u00e9s, k\u00e9rem, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/tomorrowio/translations/pt.json b/homeassistant/components/tomorrowio/translations/pt.json new file mode 100644 index 00000000000..1e8c651e5e8 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "location": "Localiza\u00e7\u00e3o", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.pt.json b/homeassistant/components/tomorrowio/translations/sensor.pt.json new file mode 100644 index 00000000000..db42eacabaa --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "tomorrowio__health_concern": { + "moderate": "Moderado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 3d00d6216a0..59174cff260 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -102,7 +102,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={ - (DOMAIN, coordinator.data.agreement.agreement_id, "meter_adapter") + ( + DOMAIN, + coordinator.data.agreement.agreement_id, + "meter_adapter", + ) # type: ignore[arg-type] }, manufacturer="Eneco", name="Meter Adapter", @@ -110,7 +114,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Spin up the platforms - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # If Home Assistant is already in a running state, register the webhook # immediately, else trigger it after Home Assistant has finished starting. diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index 9afba2bc2ae..e86d951069c 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -20,8 +20,8 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): DOMAIN = DOMAIN VERSION = 2 - agreements: list[Agreement] | None = None - data: dict[str, Any] | None = None + agreements: list[Agreement] + data: dict[str, Any] @property def logger(self) -> logging.Logger: diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index e39faa1efc6..301e3c06233 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -39,8 +39,8 @@ class ToonElectricityMeterDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Electricity Meter", - identifiers={(DOMAIN, agreement_id, "electricity")}, - via_device=(DOMAIN, agreement_id, "meter_adapter"), + identifiers={(DOMAIN, agreement_id, "electricity")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "meter_adapter"), # type: ignore[typeddict-item] ) @@ -53,8 +53,8 @@ class ToonGasMeterDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Gas Meter", - identifiers={(DOMAIN, agreement_id, "gas")}, - via_device=(DOMAIN, agreement_id, "electricity"), + identifiers={(DOMAIN, agreement_id, "gas")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "electricity"), # type: ignore[typeddict-item] ) @@ -67,8 +67,8 @@ class ToonWaterMeterDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Water Meter", - identifiers={(DOMAIN, agreement_id, "water")}, - via_device=(DOMAIN, agreement_id, "electricity"), + identifiers={(DOMAIN, agreement_id, "water")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "electricity"), # type: ignore[typeddict-item] ) @@ -81,8 +81,8 @@ class ToonSolarDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Solar Panels", - identifiers={(DOMAIN, agreement_id, "solar")}, - via_device=(DOMAIN, agreement_id, "meter_adapter"), + identifiers={(DOMAIN, agreement_id, "solar")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "meter_adapter"), # type: ignore[typeddict-item] ) @@ -96,7 +96,7 @@ class ToonBoilerModuleDeviceEntity(ToonEntity): return DeviceInfo( name="Boiler Module", manufacturer="Eneco", - identifiers={(DOMAIN, agreement_id, "boiler_module")}, + identifiers={(DOMAIN, agreement_id, "boiler_module")}, # type: ignore[arg-type] via_device=(DOMAIN, agreement_id), ) @@ -110,8 +110,8 @@ class ToonBoilerDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Boiler", - identifiers={(DOMAIN, agreement_id, "boiler")}, - via_device=(DOMAIN, agreement_id, "boiler_module"), + identifiers={(DOMAIN, agreement_id, "boiler")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "boiler_module"), # type: ignore[typeddict-item] ) diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 62829bf20ad..371672e184e 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -182,6 +182,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Gas Cost Today", section="gas_usage", measurement="day_cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=CURRENCY_EUR, icon="mdi:gas-cylinder", cls=ToonGasMeterDeviceSensor, @@ -230,6 +232,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Energy Cost Today", section="power_usage", measurement="day_cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=CURRENCY_EUR, icon="mdi:power-plug", cls=ToonElectricityMeterDeviceSensor, @@ -350,6 +354,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Water Cost Today", section="water_usage", measurement="day_cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=CURRENCY_EUR, icon="mdi:water-pump", entity_registry_enabled_default=False, diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index 1e7750b2442..65f56bffc68 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -19,7 +19,7 @@ }, "reauth_confirm": { "description": "Total Connect\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index ee82564033c..ac79e28c1b0 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0431\u0430\u0439\u043f\u0430\u0441 \u043f\u0440\u0438 \u043d\u0438\u0437\u043a\u043e\u043c \u0437\u0430\u0440\u044f\u0434\u0435 \u0431\u0430\u0442\u0430\u0440\u0435\u0438" + }, + "description": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u0437\u043e\u043d\u044b, \u043a\u043e\u0433\u0434\u0430 \u043e\u043d\u0438 \u0441\u043e\u043e\u0431\u0449\u0430\u044e\u0442 \u043e \u0440\u0430\u0437\u0440\u044f\u0434\u0435 \u0431\u0430\u0442\u0430\u0440\u0435\u0438.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 10e621e6668..50c18000baa 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from ex hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index e9cc687cc02..dfb873564c5 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -10,6 +10,10 @@ "iot_class": "local_polling", "dhcp": [ { "registered_devices": true }, + { + "hostname": "es*", + "macaddress": "54AF97*" + }, { "hostname": "ep*", "macaddress": "E848B8*" diff --git a/homeassistant/components/tplink/translations/pt.json b/homeassistant/components/tplink/translations/pt.json index 90afdfcf10a..d4507db3a55 100644 --- a/homeassistant/components/tplink/translations/pt.json +++ b/homeassistant/components/tplink/translations/pt.json @@ -2,6 +2,9 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo TP-Link encontrado na rede." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 7e103730c6d..1679965b070 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -98,7 +98,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "Traccar", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 0ed13bceefa..970cd20d640 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -1,15 +1,24 @@ """Support for Traccar device tracking.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging -from pytraccar.api import API +from pytraccar import ( + ApiClient, + DeviceModel, + GeofenceModel, + PositionModel, + TraccarAuthenticationException, + TraccarException, +) from stringcase import camelcase import voluptuous as vol from homeassistant.components.device_tracker import ( + CONF_SCAN_INTERVAL, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SOURCE_TYPE_GPS, ) @@ -21,7 +30,6 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, - CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, @@ -170,17 +178,13 @@ async def async_setup_scanner( discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Validate the configuration and return a Traccar scanner.""" - - session = async_get_clientsession(hass, config[CONF_VERIFY_SSL]) - - api = API( - hass.loop, - session, - config[CONF_USERNAME], - config[CONF_PASSWORD], - config[CONF_HOST], - config[CONF_PORT], - config[CONF_SSL], + api = ApiClient( + host=config[CONF_HOST], + port=config[CONF_PORT], + ssl=config[CONF_SSL], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + client_session=async_get_clientsession(hass, config[CONF_VERIFY_SSL]), ) scanner = TraccarScanner( @@ -202,15 +206,15 @@ class TraccarScanner: def __init__( self, - api, - hass, - async_see, - scan_interval, - max_accuracy, - skip_accuracy_on, - custom_attributes, - event_types, - ): + api: ApiClient, + hass: HomeAssistant, + async_see: Callable[..., Awaitable[None]], + scan_interval: timedelta, + max_accuracy: int, + skip_accuracy_on: bool, + custom_attributes: list[str], + event_types: list[str], + ) -> None: """Initialize.""" if EVENT_ALL_EVENTS in event_types: @@ -220,15 +224,18 @@ class TraccarScanner: self._scan_interval = scan_interval self._async_see = async_see self._api = api - self.connected = False self._hass = hass self._max_accuracy = max_accuracy self._skip_accuracy_on = skip_accuracy_on + self._devices: list[DeviceModel] = [] + self._positions: list[PositionModel] = [] + self._geofences: list[GeofenceModel] = [] async def async_init(self): """Further initialize connection to Traccar.""" - await self._api.test_connection() - if self._api.connected and not self._api.authenticated: + try: + await self._api.get_server() + except TraccarAuthenticationException: _LOGGER.error("Authentication for Traccar failed") return False @@ -238,57 +245,63 @@ class TraccarScanner: async def _async_update(self, now=None): """Update info from Traccar.""" - if not self.connected: - _LOGGER.debug("Testing connection to Traccar") - await self._api.test_connection() - self.connected = self._api.connected - if self.connected: - _LOGGER.info("Connection to Traccar restored") - else: - return _LOGGER.debug("Updating device data") - await self._api.get_device_info(self._custom_attributes) + try: + (self._devices, self._positions, self._geofences,) = await asyncio.gather( + self._api.get_devices(), + self._api.get_positions(), + self._api.get_geofences(), + ) + except TraccarException as ex: + _LOGGER.error("Error while updating device data: %s", ex) + return + self._hass.async_create_task(self.import_device_data()) if self._event_types: self._hass.async_create_task(self.import_events()) - self.connected = self._api.connected async def import_device_data(self): """Import device data from Traccar.""" - for device_unique_id in self._api.device_info: - device_info = self._api.device_info[device_unique_id] - device = None - attr = {} + for position in self._positions: + device = next( + (dev for dev in self._devices if dev.id == position.device_id), None + ) + + if not device: + continue + + attr = { + ATTR_TRACKER: "traccar", + ATTR_ADDRESS: position.address, + ATTR_SPEED: position.speed, + ATTR_ALTITUDE: position.altitude, + ATTR_MOTION: position.attributes.get("motion", False), + ATTR_TRACCAR_ID: device.id, + ATTR_GEOFENCE: next( + ( + geofence.name + for geofence in self._geofences + if geofence.id in (device.geofence_ids or []) + ), + None, + ), + ATTR_CATEGORY: device.category, + ATTR_STATUS: device.status, + } + skip_accuracy_filter = False - attr[ATTR_TRACKER] = "traccar" - if device_info.get("address") is not None: - attr[ATTR_ADDRESS] = device_info["address"] - if device_info.get("geofence") is not None: - attr[ATTR_GEOFENCE] = device_info["geofence"] - if device_info.get("category") is not None: - attr[ATTR_CATEGORY] = device_info["category"] - if device_info.get("speed") is not None: - attr[ATTR_SPEED] = device_info["speed"] - if device_info.get("motion") is not None: - attr[ATTR_MOTION] = device_info["motion"] - if device_info.get("traccar_id") is not None: - attr[ATTR_TRACCAR_ID] = device_info["traccar_id"] - for dev in self._api.devices: - if dev["id"] == device_info["traccar_id"]: - device = dev - break - if device is not None and device.get("status") is not None: - attr[ATTR_STATUS] = device["status"] for custom_attr in self._custom_attributes: - if device_info.get(custom_attr) is not None: - attr[custom_attr] = device_info[custom_attr] + if device.attributes.get(custom_attr) is not None: + attr[custom_attr] = position.attributes[custom_attr] + if custom_attr in self._skip_accuracy_on: + skip_accuracy_filter = True + if position.attributes.get(custom_attr) is not None: + attr[custom_attr] = position.attributes[custom_attr] if custom_attr in self._skip_accuracy_on: skip_accuracy_filter = True - accuracy = 0.0 - if device_info.get("accuracy") is not None: - accuracy = device_info["accuracy"] + accuracy = position.accuracy or 0.0 if ( not skip_accuracy_filter and self._max_accuracy > 0 @@ -302,42 +315,39 @@ class TraccarScanner: continue await self._async_see( - dev_id=slugify(device_info["device_id"]), - gps=(device_info.get("latitude"), device_info.get("longitude")), + dev_id=slugify(device.name), + gps=(position.latitude, position.longitude), gps_accuracy=accuracy, - battery=device_info.get("battery"), + battery=position.attributes.get("batteryLevel", -1), attributes=attr, ) async def import_events(self): """Import events from Traccar.""" - device_ids = [device["id"] for device in self._api.devices] - end_interval = datetime.utcnow() - start_interval = end_interval - self._scan_interval - events = await self._api.get_events( - device_ids=device_ids, - from_time=start_interval, - to_time=end_interval, + start_intervel = datetime.utcnow() + events = await self._api.get_reports_events( + devices=[device.id for device in self._devices], + start_time=start_intervel, + end_time=start_intervel - self._scan_interval, event_types=self._event_types.keys(), ) if events is not None: for event in events: - device_name = next( - ( - dev.get("name") - for dev in self._api.devices - if dev.get("id") == event["deviceId"] - ), - None, - ) self._hass.bus.async_fire( - f"traccar_{self._event_types.get(event['type'])}", + f"traccar_{self._event_types.get(event.type)}", { - "device_traccar_id": event["deviceId"], - "device_name": device_name, - "type": event["type"], - "serverTime": event.get("eventTime") or event.get("serverTime"), - "attributes": event["attributes"], + "device_traccar_id": event.device_id, + "device_name": next( + ( + dev.name + for dev in self._devices + if dev.id == event.device_id + ), + None, + ), + "type": event.type, + "serverTime": event.event_time, + "attributes": event.attributes, }, ) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 7f0df1b1f3f..d7b26100ab6 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -3,7 +3,7 @@ "name": "Traccar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/traccar", - "requirements": ["pytraccar==0.10.0", "stringcase==1.2.0"], + "requirements": ["pytraccar==1.0.0", "stringcase==1.2.0"], "dependencies": ["webhook"], "codeowners": ["@ludeeus"], "iot_class": "local_polling", diff --git a/homeassistant/components/traccar/translations/ja.json b/homeassistant/components/traccar/translations/ja.json index c635e23fbe0..73c7de74ced 100644 --- a/homeassistant/components/traccar/translations/ja.json +++ b/homeassistant/components/traccar/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 14783fd3f84..3761ff155b4 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -52,7 +52,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the trace integration.""" hass.data[DATA_TRACE] = {} websocket_api.async_setup(hass) - store = Store(hass, STORAGE_VERSION, STORAGE_KEY, encoder=ExtendedJSONEncoder) + store = Store[dict[str, list]]( + hass, STORAGE_VERSION, STORAGE_KEY, encoder=ExtendedJSONEncoder + ) hass.data[DATA_TRACE_STORE] = store async def _async_store_traces_at_stop(*_) -> None: diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index ff2ba9c34bf..aa5048b89c0 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -102,7 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][CLIENT] = tractive hass.data[DOMAIN][entry.entry_id][TRACKABLES] = trackables - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def cancel_listen_task(_: Event) -> None: await tractive.unsubscribe() diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index 001eb013a35..36aa2370a59 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -31,13 +31,14 @@ TRACKERS_WITH_BUILTIN_BATTERY = ("TRNJA4", "TRAXL1") class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): """Tractive sensor.""" + _attr_has_entity_name = True + def __init__( self, user_id: str, item: Trackables, description: BinarySensorEntityDescription ) -> None: """Initialize sensor entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{item.trackable['details']['name']} {description.name}" self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self.entity_description = description @@ -76,7 +77,7 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): SENSOR_TYPE = BinarySensorEntityDescription( key=ATTR_BATTERY_CHARGING, - name="Battery Charging", + name="Tracker battery charging", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, entity_category=EntityCategory.DIAGNOSTIC, ) diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 218151ae769..4b08defbf97 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -40,7 +40,9 @@ async def async_setup_entry( class TractiveDeviceTracker(TractiveEntity, TrackerEntity): """Tractive device tracker.""" + _attr_has_entity_name = True _attr_icon = "mdi:paw" + _attr_name = "Tracker" def __init__(self, user_id: str, item: Trackables) -> None: """Initialize tracker entity.""" @@ -51,8 +53,6 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): self._longitude: float = item.pos_report["latlong"][1] self._accuracy: int = item.pos_report["pos_uncertainty"] self._source_type: str = item.pos_report["sensor_used"] - - self._attr_name = f"{self._tracker_id} {item.trackable['details']['name']}" self._attr_unique_id = item.trackable["_id"] @property diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index fd29c6c6c6f..def321d928f 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -18,7 +18,7 @@ class TractiveEntity(Entity): self._attr_device_info = DeviceInfo( configuration_url="https://my.tractive.com/", identifiers={(DOMAIN, tracker_details["_id"])}, - name=f"Tractive ({tracker_details['_id']})", + name=trackable["details"]["name"], manufacturer="Tractive GmbH", sw_version=tracker_details["fw_version"], model=tracker_details["model_number"], diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index e19e10f6b44..c412502d8d9 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -48,6 +48,8 @@ class TractiveSensorEntityDescription( class TractiveSensor(TractiveEntity, SensorEntity): """Tractive sensor.""" + _attr_has_entity_name = True + def __init__( self, user_id: str, @@ -57,7 +59,6 @@ class TractiveSensor(TractiveEntity, SensorEntity): """Initialize sensor entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{item.trackable['details']['name']} {description.name}" self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self.entity_description = description @@ -133,7 +134,7 @@ class TractiveActivitySensor(TractiveSensor): SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_BATTERY_LEVEL, - name="Battery Level", + name="Tracker battery level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_class=TractiveHardwareSensor, @@ -149,14 +150,14 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( ), TractiveSensorEntityDescription( key=ATTR_MINUTES_ACTIVE, - name="Minutes Active", + name="Minutes active", icon="mdi:clock-time-eight-outline", native_unit_of_measurement=TIME_MINUTES, entity_class=TractiveActivitySensor, ), TractiveSensorEntityDescription( key=ATTR_DAILY_GOAL, - name="Daily Goal", + name="Daily goal", icon="mdi:flag-checkered", native_unit_of_measurement=TIME_MINUTES, entity_class=TractiveActivitySensor, diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index 2a425a8f2ac..87cf27b36b1 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -47,7 +47,7 @@ class TractiveSwitchEntityDescription( SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( TractiveSwitchEntityDescription( key=ATTR_BUZZER, - name="Tracker Buzzer", + name="Tracker buzzer", icon="mdi:volume-high", method="async_set_buzzer", entity_category=EntityCategory.CONFIG, @@ -61,7 +61,7 @@ SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( ), TractiveSwitchEntityDescription( key=ATTR_LIVE_TRACKING, - name="Live Tracking", + name="Live tracking", icon="mdi:map-marker-path", method="async_set_live_tracking", entity_category=EntityCategory.CONFIG, @@ -88,6 +88,7 @@ async def async_setup_entry( class TractiveSwitch(TractiveEntity, SwitchEntity): """Tractive switch.""" + _attr_has_entity_name = True entity_description: TractiveSwitchEntityDescription def __init__( @@ -99,7 +100,6 @@ class TractiveSwitch(TractiveEntity, SwitchEntity): """Initialize switch entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{item.trackable['details']['name']} {description.name}" self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self._attr_available = False self._tracker = item.tracker diff --git a/homeassistant/components/tractive/translations/hu.json b/homeassistant/components/tractive/translations/hu.json index d0f75a28ed0..5b1d9a35512 100644 --- a/homeassistant/components/tractive/translations/hu.json +++ b/homeassistant/components/tractive/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt" }, "error": { diff --git a/homeassistant/components/tractive/translations/ja.json b/homeassistant/components/tractive/translations/ja.json index 7f97d4c23f4..97defadf3ff 100644 --- a/homeassistant/components/tractive/translations/ja.json +++ b/homeassistant/components/tractive/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index e79cd8396e1..aa61be1e782 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -135,7 +135,7 @@ async def async_setup_entry( async_track_time_interval(hass, async_keep_alive, timedelta(seconds=60)) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index bd1968dfd15..a2b7304cc3e 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Callable +from collections.abc import Callable, Coroutine from functools import wraps from typing import Any, cast @@ -20,7 +20,7 @@ from .coordinator import TradfriDeviceDataUpdateCoordinator def handle_error( func: Callable[[Command | list[Command]], Any] -) -> Callable[[str], Any]: +) -> Callable[[Command | list[Command]], Coroutine[Any, Any, None]]: """Handle tradfri api call error.""" @wraps(func) diff --git a/homeassistant/components/trafikverket_ferry/__init__.py b/homeassistant/components/trafikverket_ferry/__init__.py index 5042e7c5167..c522acb6d12 100644 --- a/homeassistant/components/trafikverket_ferry/__init__.py +++ b/homeassistant/components/trafikverket_ferry/__init__.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_ferry/sensor.py b/homeassistant/components/trafikverket_ferry/sensor.py index 256341a7132..b02c673f698 100644 --- a/homeassistant/components/trafikverket_ferry/sensor.py +++ b/homeassistant/components/trafikverket_ferry/sensor.py @@ -51,7 +51,7 @@ class TrafikverketSensorEntityDescription( SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="departure_time", - name="Departure Time", + name="Departure time", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time"]), @@ -59,21 +59,21 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="departure_from", - name="Departure From", + name="Departure from", icon="mdi:ferry", value_fn=lambda data: cast(str, data["departure_from"]), info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_to", - name="Departure To", + name="Departure to", icon="mdi:ferry", value_fn=lambda data: cast(str, data["departure_to"]), info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_modified", - name="Departure Modified", + name="Departure modified", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_modified"]), @@ -82,7 +82,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="departure_time_next", - name="Departure Time Next", + name="Departure time next", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next"]), @@ -91,7 +91,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="departure_time_next_next", - name="Departure Time Next After", + name="Departure time next after", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next_next"]), @@ -121,6 +121,7 @@ class FerrySensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): entity_description: TrafikverketSensorEntityDescription _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -131,7 +132,6 @@ class FerrySensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._attr_name = f"{name} {entity_description.name}" self._attr_unique_id = f"{entry_id}-{entity_description.key}" self.entity_description = entity_description self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/trafikverket_ferry/translations/pt.json b/homeassistant/components/trafikverket_ferry/translations/pt.json new file mode 100644 index 00000000000..8cd0d6c0842 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index ee026371b04..c1c756be9ed 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "train_api": train_api, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_weatherstation/__init__.py b/homeassistant/components/trafikverket_weatherstation/__init__.py index eab54f8f45b..13b88918133 100644 --- a/homeassistant/components/trafikverket_weatherstation/__init__.py +++ b/homeassistant/components/trafikverket_weatherstation/__init__.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index c54c9f67388..68c47e9320c 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -165,6 +165,7 @@ class TrafikverketWeatherStation( entity_description: TrafikverketSensorEntityDescription _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -176,7 +177,6 @@ class TrafikverketWeatherStation( """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{sensor_station} {description.name}" self._attr_unique_id = f"{entry_id}_{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, diff --git a/homeassistant/components/trafikverket_weatherstation/translations/pt.json b/homeassistant/components/trafikverket_weatherstation/translations/pt.json new file mode 100644 index 00000000000..8cd0d6c0842 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index ae5d2dacf61..48d4a0e5163 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -168,7 +168,9 @@ class TransmissionClient: self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) def add_torrent(service: ServiceCall) -> None: """Add new torrent to download.""" diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 6f3b3191c83..f12b734a7be 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -15,7 +15,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/transmission/translations/pt.json b/homeassistant/components/transmission/translations/pt.json index c3d4131d995..399596eff8b 100644 --- a/homeassistant/components/transmission/translations/pt.json +++ b/homeassistant/components/transmission/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", @@ -9,6 +10,12 @@ "name_exists": "Nome j\u00e1 existe" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index a83e19da400..a01c71898f8 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +10,13 @@ "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index b579cc036bb..e70056f207a 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.23.0"], + "requirements": ["numpy==1.23.1"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 97422179960..9f25cfe7773 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -123,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) device_ids.add(device.id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 33b571ea848..624bfd80cd4 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -131,6 +131,7 @@ class ElectricityTypeData: class TuyaEntity(Entity): """Tuya base device.""" + _attr_has_entity_name = True _attr_should_poll = False def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: @@ -139,16 +140,6 @@ class TuyaEntity(Entity): self.device = device self.device_manager = device_manager - @property - def name(self) -> str | None: - """Return Tuya device name.""" - if ( - hasattr(self, "entity_description") - and self.entity_description.name is not None - ): - return f"{self.device.name} {self.entity_description.name}" - return self.device.name - @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index d5e4a9b22b0..5641a18022e 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -64,19 +64,19 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TuyaBinarySensorEntityDescription( key=DPCode.VOC_STATE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.PM25_STATE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.CO_STATE, - name="Carbon Monoxide", + name="Carbon monoxide", icon="mdi:molecule-co", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", @@ -84,7 +84,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.CO2_STATE, icon="mdi:molecule-co2", - name="Carbon Dioxide", + name="Carbon dioxide", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), @@ -101,7 +101,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TuyaBinarySensorEntityDescription( key=DPCode.WATERSENSOR_STATE, - name="Water Leak", + name="Water leak", device_class=BinarySensorDeviceClass.MOISTURE, on_value="alarm", ), diff --git a/homeassistant/components/tuya/button.py b/homeassistant/components/tuya/button.py index 26014e53b74..9eef7b03452 100644 --- a/homeassistant/components/tuya/button.py +++ b/homeassistant/components/tuya/button.py @@ -22,31 +22,31 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { "sd": ( ButtonEntityDescription( key=DPCode.RESET_DUSTER_CLOTH, - name="Reset Duster Cloth", + name="Reset duster cloth", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_EDGE_BRUSH, - name="Reset Edge Brush", + name="Reset edge brush", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_FILTER, - name="Reset Filter", + name="Reset filter", icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_MAP, - name="Reset Map", + name="Reset map", icon="mdi:map-marker-remove", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_ROLL_BRUSH, - name="Reset Roll Brush", + name="Reset roll brush", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 727e505200b..792036e49ff 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -433,7 +433,7 @@ UNITS = ( ), UnitOfMeasurement( unit=PERCENTAGE, - aliases={"pct", "percent"}, + aliases={"pct", "percent", "% RH"}, device_classes={ SensorDeviceClass.BATTERY, SensorDeviceClass.HUMIDITY, diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 7ba8acdc0fe..9b78008af55 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -299,7 +299,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { ), TuyaLightEntityDescription( key=DPCode.SWITCH_NIGHT_LIGHT, - name="Night Light", + name="Night light", ), ), # Remote Control diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index e7712dcf630..39874d0ae8d 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -51,21 +51,21 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.TEMP_BOILING_C, - name="Temperature After Boiling", + name="Temperature after boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_F, - name="Temperature After Boiling", + name="Temperature after boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, - name="Heat Preservation Time", + name="Heat preservation time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), @@ -80,7 +80,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.VOICE_TIMES, - name="Voice Times", + name="Voice times", icon="mdi:microphone", ), ), @@ -94,13 +94,13 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.NEAR_DETECTION, - name="Near Detection", + name="Near detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.FAR_DETECTION, - name="Far Detection", + name="Far detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), @@ -110,7 +110,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "kfj": ( NumberEntityDescription( key=DPCode.WATER_SET, - name="Water Level", + name="Water level", icon="mdi:cup-water", entity_category=EntityCategory.CONFIG, ), @@ -123,7 +123,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.WARM_TIME, - name="Heat Preservation Time", + name="Heat preservation time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), @@ -138,20 +138,20 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "mzj": ( NumberEntityDescription( key=DPCode.COOK_TEMPERATURE, - name="Cook Temperature", + name="Cook temperature", icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.COOK_TIME, - name="Cook Time", + name="Cook time", icon="mdi:timer", native_unit_of_measurement=TIME_MINUTES, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLOUD_RECIPE_NUMBER, - name="Cloud Recipe", + name="Cloud recipe", entity_category=EntityCategory.CONFIG, ), ), @@ -189,37 +189,37 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "tgkg": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, - name="Minimum Brightness", + name="Minimum brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, - name="Maximum Brightness", + name="Maximum brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, - name="Minimum Brightness 2", + name="Minimum brightness 2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, - name="Maximum Brightness 2", + name="Maximum brightness 2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_3, - name="Minimum Brightness 3", + name="Minimum brightness 3", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_3, - name="Maximum Brightness 3", + name="Maximum brightness 3", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), @@ -229,25 +229,25 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "tgq": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, - name="Minimum Brightness", + name="Minimum brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, - name="Maximum Brightness", + name="Maximum brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, - name="Minimum Brightness 2", + name="Minimum brightness 2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, - name="Maximum Brightness 2", + name="Maximum brightness 2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), @@ -265,19 +265,19 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "szjqr": ( NumberEntityDescription( key=DPCode.ARM_DOWN_PERCENT, - name="Move Down %", + name="Move down %", icon="mdi:arrow-down-bold", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.ARM_UP_PERCENT, - name="Move Up %", + name="Move up %", icon="mdi:arrow-up-bold", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLICK_SUSTAIN_TIME, - name="Down Delay", + name="Down delay", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 974268c109f..6ec636b078d 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -57,13 +57,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "kg": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on Behavior", + name="Power on behavior", device_class=TuyaDeviceClass.RELAY_STATUS, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator Light Mode", + name="Indicator light mode", device_class=TuyaDeviceClass.LIGHT_MODE, entity_category=EntityCategory.CONFIG, ), @@ -73,7 +73,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "qn": ( SelectEntityDescription( key=DPCode.LEVEL, - name="Temperature Level", + name="Temperature level", icon="mdi:thermometer-lines", ), ), @@ -96,27 +96,27 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "sp": ( SelectEntityDescription( key=DPCode.IPC_WORK_MODE, - name="IPC Mode", + name="IPC mode", device_class=TuyaDeviceClass.IPC_WORK_MODE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.DECIBEL_SENSITIVITY, - name="Sound Detection Sensitivity", + name="Sound detection densitivity", icon="mdi:volume-vibrate", device_class=TuyaDeviceClass.DECIBEL_SENSITIVITY, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.RECORD_MODE, - name="Record Mode", + name="Record mode", icon="mdi:record-rec", device_class=TuyaDeviceClass.RECORD_MODE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.BASIC_NIGHTVISION, - name="Night Vision", + name="Night vision", icon="mdi:theme-light-dark", device_class=TuyaDeviceClass.BASIC_NIGHTVISION, entity_category=EntityCategory.CONFIG, @@ -130,7 +130,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { ), SelectEntityDescription( key=DPCode.MOTION_SENSITIVITY, - name="Motion Detection Sensitivity", + name="Motion detection sensitivity", icon="mdi:motion-sensor", device_class=TuyaDeviceClass.MOTION_SENSITIVITY, entity_category=EntityCategory.CONFIG, @@ -141,13 +141,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "tdq": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on Behavior", + name="Power on behavior", device_class=TuyaDeviceClass.RELAY_STATUS, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator Light Mode", + name="Indicator light mode", device_class=TuyaDeviceClass.LIGHT_MODE, entity_category=EntityCategory.CONFIG, ), @@ -157,31 +157,31 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "tgkg": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on Behavior", + name="Power on behavior", device_class=TuyaDeviceClass.RELAY_STATUS, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator Light Mode", + name="Indicator light mode", device_class=TuyaDeviceClass.LIGHT_MODE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_1, - name="Light Source Type", + name="Light source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_2, - name="Light 2 Source Type", + name="Light 2 source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_3, - name="Light 3 Source Type", + name="Light 3 source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), @@ -191,13 +191,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "tgq": ( SelectEntityDescription( key=DPCode.LED_TYPE_1, - name="Light Source Type", + name="Light source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_2, - name="Light 2 Source Type", + name="Light 2 source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), @@ -216,14 +216,14 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "sd": ( SelectEntityDescription( key=DPCode.CISTERN, - name="Water Tank Adjustment", + name="Water tank adjustment", entity_category=EntityCategory.CONFIG, device_class=TuyaDeviceClass.VACUUM_CISTERN, icon="mdi:water-opacity", ), SelectEntityDescription( key=DPCode.COLLECTION_MODE, - name="Dust Collection Mode", + name="Dust collection mode", entity_category=EntityCategory.CONFIG, device_class=TuyaDeviceClass.VACUUM_COLLECTION, icon="mdi:air-filter", @@ -241,14 +241,14 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "fs": ( SelectEntityDescription( key=DPCode.FAN_VERTICAL, - name="Vertical Swing Flap Angle", + name="Vertical swing flap angle", device_class=TuyaDeviceClass.FAN_ANGLE, entity_category=EntityCategory.CONFIG, icon="mdi:format-vertical-align-center", ), SelectEntityDescription( key=DPCode.FAN_HORIZONTAL, - name="Horizontal Swing Flap Angle", + name="Horizontal swing flap angle", device_class=TuyaDeviceClass.FAN_ANGLE, entity_category=EntityCategory.CONFIG, icon="mdi:format-horizontal-align-center", @@ -273,7 +273,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "cl": ( SelectEntityDescription( key=DPCode.CONTROL_BACK_MODE, - name="Motor Mode", + name="Motor mode", device_class=TuyaDeviceClass.CURTAIN_MOTOR_MODE, entity_category=EntityCategory.CONFIG, icon="mdi:swap-horizontal", @@ -290,14 +290,14 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "jsq": ( SelectEntityDescription( key=DPCode.SPRAY_MODE, - name="Spray Mode", + name="Spray mode", device_class=TuyaDeviceClass.HUMIDIFIER_SPRAY_MODE, entity_category=EntityCategory.CONFIG, icon="mdi:spray", ), SelectEntityDescription( key=DPCode.LEVEL, - name="Spraying Level", + name="Spraying level", device_class=TuyaDeviceClass.HUMIDIFIER_LEVEL, entity_category=EntityCategory.CONFIG, icon="mdi:spray", diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index dd2996f61ba..266ee951530 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -58,7 +58,7 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( ), TuyaSensorEntityDescription( key=DPCode.BATTERY_STATE, - name="Battery State", + name="Battery state", icon="mdi:battery", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -99,26 +99,26 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO_VALUE, - name="Carbon Monoxide", + name="Carbon monoxide", icon="mdi:molecule-co", device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", icon="mdi:molecule-co2", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, @@ -154,7 +154,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, - name="Smoke Amount", + name="Smoke amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorStateClass.MEASUREMENT, @@ -166,13 +166,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "bh": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Current Temperature", + name="Current temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT_F, - name="Current Temperature", + name="Current temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), @@ -199,7 +199,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -210,7 +210,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cobj": ( TuyaSensorEntityDescription( key=DPCode.CO_VALUE, - name="Carbon Monoxide", + name="Carbon monoxide", device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), @@ -221,7 +221,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cwwsq": ( TuyaSensorEntityDescription( key=DPCode.FEED_REPORT, - name="Last Amount", + name="Last amount", icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, ), @@ -243,7 +243,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -254,13 +254,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -270,19 +270,19 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "jqbj": ( TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -368,7 +368,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -385,7 +385,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "mzj": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Current Temperature", + name="Current temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), @@ -396,7 +396,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.REMAIN_TIME, - name="Remaining Time", + name="Remaining time", native_unit_of_measurement=TIME_MINUTES, icon="mdi:timer", ), @@ -409,7 +409,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "pm2.5": ( TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -420,7 +420,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), @@ -432,7 +432,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -444,13 +444,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PM1, - name="Particulate Matter 1.0 µm", + name="Particulate matter 1.0 µm", device_class=SensorDeviceClass.PM1, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM10, - name="Particulate Matter 10.0 µm", + name="Particulate matter 10.0 µm", device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), @@ -515,13 +515,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "voc": ( TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -544,7 +544,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), @@ -603,7 +603,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "ywbj": ( TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, - name="Smoke Amount", + name="Smoke amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorStateClass.MEASUREMENT, @@ -618,13 +618,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "zndb": ( TuyaSensorEntityDescription( key=DPCode.FORWARD_ENERGY_TOTAL, - name="Total Energy", + name="Total energy", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Current", + name="Phase A current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -632,7 +632,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Power", + name="Phase A power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -640,7 +640,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Voltage", + name="Phase A voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -648,7 +648,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Current", + name="Phase B current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -656,7 +656,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Power", + name="Phase B power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -664,7 +664,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Voltage", + name="Phase B voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -672,7 +672,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Current", + name="Phase C current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -680,7 +680,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Power", + name="Phase C power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -688,7 +688,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Voltage", + name="Phase C voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -700,13 +700,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "dlq": ( TuyaSensorEntityDescription( key=DPCode.TOTAL_FORWARD_ENERGY, - name="Total Energy", + name="Total energy", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Current", + name="Phase A current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -714,7 +714,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Power", + name="Phase A power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -722,7 +722,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Voltage", + name="Phase A voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -730,7 +730,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Current", + name="Phase B current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -738,7 +738,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Power", + name="Phase B power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -746,7 +746,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Voltage", + name="Phase B voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -754,7 +754,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Current", + name="Phase C current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -762,7 +762,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Power", + name="Phase C power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -770,7 +770,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Voltage", + name="Phase C voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -782,13 +782,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "sd": ( TuyaSensorEntityDescription( key=DPCode.CLEAN_AREA, - name="Cleaning Area", + name="Cleaning area", icon="mdi:texture-box", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CLEAN_TIME, - name="Cleaning Time", + name="Cleaning time", icon="mdi:progress-clock", state_class=SensorStateClass.MEASUREMENT, ), @@ -800,37 +800,37 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_TIME, - name="Total Cleaning Time", + name="Total cleaning time", icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_COUNT, - name="Total Cleaning Times", + name="Total cleaning times", icon="mdi:counter", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.DUSTER_CLOTH, - name="Duster Cloth Life", + name="Duster cloth life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.EDGE_BRUSH, - name="Side Brush Life", + name="Side brush life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.FILTER_LIFE, - name="Filter Life", + name="Filter life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ROLL_BRUSH, - name="Rolling Brush Life", + name="Rolling brush life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), @@ -840,7 +840,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cl": ( TuyaSensorEntityDescription( key=DPCode.TIME_TOTAL, - name="Last Operation Duration", + name="Last operation duration", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:progress-clock", ), @@ -868,7 +868,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.LEVEL_CURRENT, - name="Water Level", + name="Water level", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:waves-arrow-up", ), @@ -878,13 +878,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "kj": ( TuyaSensorEntityDescription( key=DPCode.FILTER, - name="Filter Utilization", + name="Filter utilization", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:ticket-percent-outline", ), TuyaSensorEntityDescription( key=DPCode.PM25, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, icon="mdi:molecule", @@ -903,26 +903,26 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.TVOC, - name="Total Volatile Organic Compound", + name="Total volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ECO2, - name="Concentration of Carbon Dioxide", + name="Concentration of carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_TIME, - name="Total Operating Time", + name="Total operating time", icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_PM, - name="Total Absorption of Particles", + name="Total absorption of particles", icon="mdi:texture-box", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/tuya/translations/select.pt.json b/homeassistant/components/tuya/translations/select.pt.json new file mode 100644 index 00000000000..3a66717f219 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.pt.json @@ -0,0 +1,26 @@ +{ + "state": { + "tuya__basic_nightvision": { + "1": "Desligado" + }, + "tuya__countdown": { + "cancel": "Cancelar" + }, + "tuya__light_mode": { + "none": "Desligado" + }, + "tuya__record_mode": { + "2": "Grava\u00e7\u00e3o cont\u00ednua" + }, + "tuya__relay_status": { + "off": "Desligado", + "on": "Ligado", + "power_off": "Desligado" + }, + "tuya__vacuum_mode": { + "pick_zone": "Escolha a Zona", + "right_spiral": "Espiral Direita", + "spiral": "Espiral" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.pt.json b/homeassistant/components/tuya/translations/sensor.pt.json new file mode 100644 index 00000000000..9547600f35f --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "tuya__air_quality": { + "good": "Bom" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index bbe306392ba..c4fe53c67f0 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data.setdefault(DOMAIN, {})[entry.data[CONF_ID]] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py index 1c7cfcbd4fc..8bcf1b1d390 100644 --- a/homeassistant/components/twentemilieu/calendar.py +++ b/homeassistant/components/twentemilieu/calendar.py @@ -28,7 +28,7 @@ async def async_setup_entry( class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity): """Defines a Twente Milieu calendar.""" - _attr_name = "Twente Milieu" + _attr_has_entity_name = True _attr_icon = "mdi:delete-empty" def __init__( diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index c9f2f935772..ac7354a42f2 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -15,9 +15,9 @@ CONF_HOUSE_NUMBER = "house_number" CONF_HOUSE_LETTER = "house_letter" WASTE_TYPE_TO_DESCRIPTION = { - WasteType.NON_RECYCLABLE: "Non-recyclable Waste Pickup", - WasteType.ORGANIC: "Organic Waste Pickup", - WasteType.PACKAGES: "Packages Waste Pickup", - WasteType.PAPER: "Paper Waste Pickup", - WasteType.TREE: "Christmas Tree Pickup", + WasteType.NON_RECYCLABLE: "Non-recyclable waste pickup", + WasteType.ORGANIC: "Organic waste pickup", + WasteType.PACKAGES: "Packages waste pickup", + WasteType.PAPER: "Paper waste pickup", + WasteType.TREE: "Christmas tree pickup", } diff --git a/homeassistant/components/twentemilieu/entity.py b/homeassistant/components/twentemilieu/entity.py index 008c0fa441e..5a1a1758d3e 100644 --- a/homeassistant/components/twentemilieu/entity.py +++ b/homeassistant/components/twentemilieu/entity.py @@ -22,6 +22,8 @@ class TwenteMilieuEntity( ): """Defines a Twente Milieu entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]], diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json index 36ea2123bdb..42e9f0b3e23 100644 --- a/homeassistant/components/twentemilieu/translations/de.json +++ b/homeassistant/components/twentemilieu/translations/de.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "house_letter": "Hausbrief/zusatz", + "house_letter": "Hausbrief/Zusatz", "house_number": "Hausnummer", "post_code": "Postleitzahl" }, diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json index 521fee184f2..4ebff4fde6e 100644 --- a/homeassistant/components/twilio/translations/ja.json +++ b/homeassistant/components/twilio/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index a8347889895..3b0228e64b0 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = client hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_INFO] = device_info - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py index 587854a3a7e..eb24e5d9a78 100644 --- a/homeassistant/components/ukraine_alarm/__init__.py +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ukraine_alarm/translations/ca.json b/homeassistant/components/ukraine_alarm/translations/ca.json index ea03b64dd3b..0c5a5e3dedb 100644 --- a/homeassistant/components/ukraine_alarm/translations/ca.json +++ b/homeassistant/components/ukraine_alarm/translations/ca.json @@ -5,7 +5,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "max_regions": "Es poden configurar un m\u00e0xim de 5 regions", "rate_limit": "Massa peticions", - "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", + "timeout": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json index 81bdabdc8d3..ae57ebe0d17 100644 --- a/homeassistant/components/ukraine_alarm/translations/ja.json +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -25,7 +25,7 @@ "data": { "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, - "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "\u30e2\u30cb\u30bf\u30fc\u3059\u308b\u72b6\u614b\u3092\u9078\u629e" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/pt.json b/homeassistant/components/ukraine_alarm/translations/pt.json new file mode 100644 index 00000000000..e6c831ab07c --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "timeout": "Tempo limite para estabelecer liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 7a874aff993..086bae8d8cf 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,20 +1,17 @@ """Integration to UniFi Network and its various features.""" +from collections.abc import Mapping +from typing import Any + from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from .const import ( - ATTR_MANUFACTURER, - CONF_CONTROLLER, - DOMAIN as UNIFI_DOMAIN, - LOGGER, - UNIFI_WIRELESS_CLIENTS, -) -from .controller import UniFiController +from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN, UNIFI_WIRELESS_CLIENTS +from .controller import PLATFORMS, UniFiController, get_unifi_controller +from .errors import AuthenticationRequired, CannotConnect from .services import async_setup_services, async_unload_services SAVE_DELAY = 10 @@ -37,9 +34,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Flat configuration was introduced with 2021.3 await async_flatten_entry_data(hass, config_entry) - controller = UniFiController(hass, config_entry) - if not await controller.async_setup(): - return False + try: + api = await get_unifi_controller(hass, config_entry.data) + controller = UniFiController(hass, config_entry, api) + await controller.initialize() + + except CannotConnect as err: + raise ConfigEntryNotReady from err + + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err # Unique ID was introduced with 2021.3 if config_entry.unique_id is None: @@ -47,30 +51,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry, unique_id=controller.site_id ) - if not hass.data[UNIFI_DOMAIN]: + hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + await controller.async_update_device_registry() + + if len(hass.data[UNIFI_DOMAIN]) == 1: async_setup_services(hass) - hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller + api.start_websocket() config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) ) - LOGGER.debug("UniFi Network config options %s", config_entry.options) - - if controller.mac is None: - return True - - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - configuration_url=controller.api.url, - connections={(CONNECTION_NETWORK_MAC, controller.mac)}, - default_manufacturer=ATTR_MANUFACTURER, - default_model="UniFi Network", - default_name="UniFi Network", - ) - return True @@ -92,7 +85,10 @@ async def async_flatten_entry_data( Keep controller key layer in case user rollbacks. """ - data: dict = {**config_entry.data, **config_entry.data[CONF_CONTROLLER]} + data: Mapping[str, Any] = { + **config_entry.data, + **config_entry.data[CONF_CONTROLLER], + } if config_entry.data != data: hass.config_entries.async_update_entry(config_entry, data=data) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index fcf5970bf6c..4944dd91296 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -9,6 +9,7 @@ from __future__ import annotations from collections.abc import Mapping import socket +from types import MappingProxyType from typing import Any from urllib.parse import urlparse @@ -46,7 +47,7 @@ from .const import ( DEFAULT_POE_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) -from .controller import get_controller +from .controller import UniFiController, get_unifi_controller from .errors import AuthenticationRequired, CannotConnect DEFAULT_PORT = 443 @@ -75,11 +76,11 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def __init__(self) -> None: """Initialize the UniFi Network flow.""" - self.config = {} - self.site_ids = {} - self.site_names = {} - self.reauth_config_entry = None - self.reauth_schema = {} + self.config: dict[str, Any] = {} + self.site_ids: dict[str, str] = {} + self.site_names: dict[str, str] = {} + self.reauth_config_entry: config_entries.ConfigEntry | None = None + self.reauth_schema: dict[vol.Marker, Any] = {} async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -99,16 +100,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): } try: - controller = await get_controller( - self.hass, - host=self.config[CONF_HOST], - username=self.config[CONF_USERNAME], - password=self.config[CONF_PASSWORD], - port=self.config[CONF_PORT], - site=self.config[CONF_SITE_ID], - verify_ssl=self.config[CONF_VERIFY_SSL], + controller = await get_unifi_controller( + self.hass, MappingProxyType(self.config) ) - sites = await controller.sites() except AuthenticationRequired: @@ -156,8 +150,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Select site to control.""" - errors = {} - if user_input is not None: unique_id = user_input[CONF_SITE_ID] @@ -173,9 +165,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): abort_reason = "reauth_successful" if config_entry: - controller = self.hass.data.get(UNIFI_DOMAIN, {}).get( - config_entry.entry_id - ) + controller: UniFiController | None = self.hass.data.get( + UNIFI_DOMAIN, {} + ).get(config_entry.entry_id) if controller and controller.available: return self.async_abort(reason="already_configured") @@ -199,7 +191,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): data_schema=vol.Schema( {vol.Required(CONF_SITE_ID): vol.In(self.site_names)} ), - errors=errors, ) async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: @@ -207,6 +198,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): config_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) + assert config_entry self.reauth_config_entry = config_entry self.context["title_placeholders"] = { @@ -258,11 +250,12 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): class UnifiOptionsFlowHandler(config_entries.OptionsFlow): """Handle Unifi Network options.""" + controller: UniFiController + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize UniFi Network options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) - self.controller = None async def async_step_init( self, user_input: dict[str, Any] | None = None @@ -379,8 +372,6 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage configuration of network access controlled clients.""" - errors = {} - if user_input is not None: self.options.update(user_input) return await self.async_step_statistics_sensors() @@ -417,7 +408,6 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): ): bool, } ), - errors=errors, last_step=False, ) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index fa92568b477..7446d6abbff 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -4,6 +4,8 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta import ssl +from types import MappingProxyType +from typing import Any from aiohttp import CookieJar import aiounifi @@ -36,14 +38,19 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, entity_registry as er +from homeassistant.helpers import ( + aiohttp_client, + device_registry as dr, + entity_registry as er, +) +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.dt as dt_util from .const import ( + ATTR_MANUFACTURER, CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, @@ -91,12 +98,15 @@ DEVICE_CONNECTED = ( class UniFiController: """Manages a single UniFi Network instance.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry, api): """Initialize the system.""" self.hass = hass self.config_entry = config_entry + self.api = api + + api.callback = self.async_unifi_signalling_callback + self.available = True - self.api = None self.progress = None self.wireless_clients = None @@ -295,36 +305,18 @@ class UniFiController: unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry) - async def async_setup(self): + async def initialize(self): """Set up a UniFi Network instance.""" - try: - self.api = await get_controller( - self.hass, - host=self.config_entry.data[CONF_HOST], - username=self.config_entry.data[CONF_USERNAME], - password=self.config_entry.data[CONF_PASSWORD], - port=self.config_entry.data[CONF_PORT], - site=self.config_entry.data[CONF_SITE_ID], - verify_ssl=self.config_entry.data[CONF_VERIFY_SSL], - async_callback=self.async_unifi_signalling_callback, - ) - await self.api.initialize() - - sites = await self.api.sites() - description = await self.api.site_description() - - except CannotConnect as err: - raise ConfigEntryNotReady from err - - except AuthenticationRequired as err: - raise ConfigEntryAuthFailed from err + await self.api.initialize() + sites = await self.api.sites() for site in sites.values(): if self.site == site["name"]: self.site_id = site["_id"] self._site_name = site["desc"] break + description = await self.api.site_description() self._site_role = description[0]["site_role"] # Restore clients that are not a part of active clients list. @@ -357,18 +349,12 @@ class UniFiController: self.wireless_clients = wireless_clients.get_data(self.config_entry) self.update_wireless_clients() - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) - - self.api.start_websocket() - self.config_entry.add_update_listener(self.async_config_entry_updated) self._cancel_heartbeat_check = async_track_time_interval( self.hass, self._async_check_for_stale, CHECK_HEARTBEAT_INTERVAL ) - return True - @callback def async_heartbeat( self, unique_id: str, heartbeat_expire_time: datetime | None = None @@ -397,6 +383,22 @@ class UniFiController: for unique_id in unique_ids_to_remove: del self._heartbeat_time[unique_id] + async def async_update_device_registry(self) -> None: + """Update device registry.""" + if self.mac is None: + return + + device_registry = dr.async_get(self.hass) + + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + configuration_url=self.api.url, + connections={(CONNECTION_NETWORK_MAC, self.mac)}, + default_manufacturer=ATTR_MANUFACTURER, + default_model="UniFi Network", + default_name="UniFi Network", + ) + @staticmethod async def async_config_entry_updated( hass: HomeAssistant, config_entry: ConfigEntry @@ -463,13 +465,14 @@ class UniFiController: return True -async def get_controller( - hass, host, username, password, port, site, verify_ssl, async_callback=None -): +async def get_unifi_controller( + hass: HomeAssistant, + config: MappingProxyType[str, Any], +) -> aiounifi.Controller: """Create a controller object and verify authentication.""" sslcontext = None - if verify_ssl: + if verify_ssl := bool(config.get(CONF_VERIFY_SSL)): session = aiohttp_client.async_get_clientsession(hass) if isinstance(verify_ssl, str): sslcontext = ssl.create_default_context(cafile=verify_ssl) @@ -479,14 +482,13 @@ async def get_controller( ) controller = aiounifi.Controller( - host, - username=username, - password=password, - port=port, - site=site, + host=config[CONF_HOST], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + port=config[CONF_PORT], + site=config[CONF_SITE_ID], websession=session, sslcontext=sslcontext, - callback=async_callback, ) try: @@ -498,7 +500,7 @@ async def get_controller( except aiounifi.Unauthorized as err: LOGGER.warning( "Connected to UniFi Network at %s but not registered: %s", - host, + config[CONF_HOST], err, ) raise AuthenticationRequired from err @@ -510,13 +512,15 @@ async def get_controller( aiounifi.RequestError, aiounifi.ResponseError, ) as err: - LOGGER.error("Error connecting to the UniFi Network at %s: %s", host, err) + LOGGER.error( + "Error connecting to the UniFi Network at %s: %s", config[CONF_HOST], err + ) raise CannotConnect from err except aiounifi.LoginRequired as err: LOGGER.warning( "Connected to UniFi Network at %s but login required: %s", - host, + config[CONF_HOST], err, ) raise AuthenticationRequired from err diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b2070362d02..f2c2230b9e0 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -27,7 +27,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN -from .unifi_client import UniFiClient +from .controller import UniFiController +from .unifi_client import UniFiClientBase from .unifi_entity_base import UniFiBase LOGGER = logging.getLogger(__name__) @@ -79,7 +80,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up device tracker for UniFi Network integration.""" - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = {CLIENT_TRACKER: set(), DEVICE_TRACKER: set()} @callback @@ -144,7 +145,7 @@ def add_device_entities(controller, async_add_entities, devices): async_add_entities(trackers) -class UniFiClientTracker(UniFiClient, ScannerEntity): +class UniFiClientTracker(UniFiClientBase, ScannerEntity): """Representation of a network client.""" DOMAIN = DOMAIN @@ -261,11 +262,6 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): self.async_write_ha_state() self._async_log_debug_data("make_disconnected") - @property - def device_info(self) -> None: - """Return no device info.""" - return None - @property def is_connected(self): """Return true if the client is connected to the network.""" diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index ed059856881..b35fd520ab0 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import format_mac from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN +from .controller import UniFiController TO_REDACT = {CONF_CONTROLLER, CONF_PASSWORD} REDACT_CONFIG = {CONF_CONTROLLER, CONF_HOST, CONF_PASSWORD, CONF_USERNAME} @@ -56,7 +57,7 @@ def async_replace_list_data( """Redact sensitive data in a list.""" redacted = [] for item in data: - new_value = None + new_value: Any | None = None if isinstance(item, (list, set, tuple)): new_value = async_replace_list_data(item, to_replace) elif isinstance(item, Mapping): @@ -74,7 +75,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] diag: dict[str, Any] = {} macs_to_redact: dict[str, str] = {} diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index fba092385c1..cd810e12d6a 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -27,7 +27,7 @@ }, "options": { "abort": { - "integration_not_setup": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "integration_not_setup": "UniFi\u7d71\u5408\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3066\u3044\u307e\u305b\u3093" }, "step": { "client_control": { @@ -57,7 +57,7 @@ "track_clients": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308b", "track_devices": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1(\u30e6\u30d3\u30ad\u30c6\u30a3\u30c7\u30d0\u30a4\u30b9)" }, - "description": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" + "description": "UniFi\u7d71\u5408\u306e\u8a2d\u5b9a" }, "statistics_sensors": { "data": { diff --git a/homeassistant/components/unifi/translations/pt.json b/homeassistant/components/unifi/translations/pt.json index 7a0a8e1a1fd..b62d5f4104e 100644 --- a/homeassistant/components/unifi/translations/pt.json +++ b/homeassistant/components/unifi/translations/pt.json @@ -15,7 +15,7 @@ "port": "Porto", "site": "Site ID", "username": "Nome do utilizador", - "verify_ssl": "Controlador com certificados adequados" + "verify_ssl": "Verificar o certificado SSL" }, "title": "Configurar o controlador UniFi" } diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index 9e90eef518a..82aece81b6d 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -5,8 +5,8 @@ from homeassistant.helpers.entity import DeviceInfo from .unifi_entity_base import UniFiBase -class UniFiClient(UniFiBase): - """Base class for UniFi clients.""" +class UniFiClientBase(UniFiBase): + """Base class for UniFi clients (without device info).""" def __init__(self, client, controller) -> None: """Set up client.""" @@ -44,6 +44,10 @@ class UniFiClient(UniFiBase): """Return if controller is available.""" return self.controller.available + +class UniFiClient(UniFiClientBase): + """Base class for UniFi clients (with device info).""" + @property def device_info(self) -> DeviceInfo: """Return a client description for device registry.""" diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index c611fc1ee60..466764714cf 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -1,12 +1,18 @@ """Base class for UniFi Network entities.""" +from __future__ import annotations + +from collections.abc import Callable import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.core import callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +if TYPE_CHECKING: + from .controller import UniFiController + _LOGGER = logging.getLogger(__name__) @@ -16,7 +22,7 @@ class UniFiBase(Entity): DOMAIN = "" TYPE = "" - def __init__(self, item, controller) -> None: + def __init__(self, item, controller: UniFiController) -> None: """Set up UniFi Network entity base. Register mac to controller entities to cover disabled entities. @@ -38,11 +44,12 @@ class UniFiBase(Entity): self.entity_id, self.key, ) - for signal, method in ( + signals: tuple[tuple[str, Callable[..., Any]], ...] = ( (self.controller.signal_reachable, self.async_signal_reachable_callback), (self.controller.signal_options_update, self.options_updated), (self.controller.signal_remove, self.remove_item), - ): + ) + for signal, method in signals: self.async_on_remove(async_dispatcher_connect(self.hass, signal, method)) self._item.register_callback(self.async_update_callback) diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 09720f15f84..24967e043d9 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.update import ( DOMAIN, @@ -71,7 +72,6 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): DOMAIN = DOMAIN TYPE = DEVICE_UPDATE _attr_device_class = UpdateDeviceClass.FIRMWARE - _attr_supported_features = UpdateEntityFeature.PROGRESS def __init__(self, device, controller): """Set up device update entity.""" @@ -79,6 +79,11 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): self.device = self._item + self._attr_supported_features = UpdateEntityFeature.PROGRESS + + if self.controller.site_role == "admin": + self._attr_supported_features |= UpdateEntityFeature.INSTALL + @property def name(self) -> str: """Return the name of the device.""" @@ -126,3 +131,9 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): async def options_updated(self) -> None: """No action needed.""" + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + await self.controller.api.devices.upgrade(self.device.mac) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 40214b60766..30b1d1ad56d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -40,6 +40,7 @@ from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services from .utils import _async_unifi_mac_from_hass, async_get_devices +from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) @@ -90,8 +91,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async_setup_services(hass) + hass.http.register_view(ThumbnailProxyView(hass)) + hass.http.register_view(VideoProxyView(hass)) entry.async_on_unload(entry.add_update_listener(_async_options_updated)) entry.async_on_unload( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 35dafc96927..815b5250e1d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.11", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index 11e0151f165..cbef293c7e9 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -16,7 +16,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ipaddress})? Egy helyi felhaszn\u00e1l\u00f3t kell l\u00e9trehoznia Unifi OS-ben.", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ipaddress})? A bejelentkez\u00e9shez egy helyi felhaszn\u00e1l\u00f3ra lesz sz\u00fcks\u00e9g, amelyet az UniFi OS Console-ban hoztak l\u00e9tre. Az Ubiquiti Cloud Users nem fog m\u0171k\u00f6dni. Tov\u00e1bbi inform\u00e1ci\u00f3: {local_user_documentation_url}", "title": "UniFi Protect felfedezve" }, "reauth_confirm": { diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index 1d0fff95c3a..8ea65342e78 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "discovery_started": "Rilevamento " + "discovery_started": "Rilevamento iniziato" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/unifiprotect/translations/pt.json b/homeassistant/components/unifiprotect/translations/pt.json new file mode 100644 index 00000000000..9f781c2de55 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/pt.json @@ -0,0 +1,26 @@ +{ + "config": { + "flow_title": "{name} ({ip_address})", + "step": { + "discovery_confirm": { + "data": { + "username": "Nome de Utilizador" + }, + "description": "Do you want to setup {name} ({ip_address})? " + }, + "reauth_confirm": { + "data": { + "port": "Porta" + } + }, + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "port": "Porta", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py new file mode 100644 index 00000000000..ea523d36dd2 --- /dev/null +++ b/homeassistant/components/unifiprotect/views.py @@ -0,0 +1,211 @@ +"""UniFi Protect Integration views.""" +from __future__ import annotations + +from datetime import datetime +from http import HTTPStatus +import logging +from typing import Any +from urllib.parse import urlencode + +from aiohttp import web +from pyunifiprotect.data import Event +from pyunifiprotect.exceptions import ClientError + +from homeassistant.components.http import HomeAssistantView +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN +from .data import ProtectData + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_generate_thumbnail_url( + event_id: str, + nvr_id: str, + width: int | None = None, + height: int | None = None, +) -> str: + """Generate URL for event thumbnail.""" + + url_format = ThumbnailProxyView.url or "{nvr_id}/{event_id}" + url = url_format.format(nvr_id=nvr_id, event_id=event_id) + + params = {} + if width is not None: + params["width"] = str(width) + if height is not None: + params["height"] = str(height) + + return f"{url}?{urlencode(params)}" + + +@callback +def async_generate_event_video_url(event: Event) -> str: + """Generate URL for event video.""" + + _validate_event(event) + if event.start is None or event.end is None: + raise ValueError("Event is ongoing") + + url_format = VideoProxyView.url or "{nvr_id}/{camera_id}/{start}/{end}" + url = url_format.format( + nvr_id=event.api.bootstrap.nvr.id, + camera_id=event.camera_id, + start=event.start.isoformat(), + end=event.end.isoformat(), + ) + + return url + + +@callback +def _client_error(message: Any, code: HTTPStatus) -> web.Response: + _LOGGER.warning("Client error (%s): %s", code.value, message) + if code == HTTPStatus.BAD_REQUEST: + return web.Response(body=message, status=code) + return web.Response(status=code) + + +@callback +def _400(message: Any) -> web.Response: + return _client_error(message, HTTPStatus.BAD_REQUEST) + + +@callback +def _403(message: Any) -> web.Response: + return _client_error(message, HTTPStatus.FORBIDDEN) + + +@callback +def _404(message: Any) -> web.Response: + return _client_error(message, HTTPStatus.NOT_FOUND) + + +@callback +def _validate_event(event: Event) -> None: + if event.camera is None: + raise ValueError("Event does not have a camera") + if not event.camera.can_read_media(event.api.bootstrap.auth_user): + raise PermissionError(f"User cannot read media from camera: {event.camera.id}") + + +class ProtectProxyView(HomeAssistantView): + """Base class to proxy request to UniFi Protect console.""" + + requires_auth = True + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize a thumbnail proxy view.""" + self.hass = hass + self.data = hass.data[DOMAIN] + + def _get_data_or_404(self, nvr_id: str) -> ProtectData | web.Response: + all_data: list[ProtectData] = [] + + for data in self.data.values(): + if isinstance(data, ProtectData): + if data.api.bootstrap.nvr.id == nvr_id: + return data + all_data.append(data) + return _404("Invalid NVR ID") + + +class ThumbnailProxyView(ProtectProxyView): + """View to proxy event thumbnails from UniFi Protect.""" + + url = "/api/unifiprotect/thumbnail/{nvr_id}/{event_id}" + name = "api:unifiprotect_thumbnail" + + async def get( + self, request: web.Request, nvr_id: str, event_id: str + ) -> web.Response: + """Get Event Thumbnail.""" + + data = self._get_data_or_404(nvr_id) + if isinstance(data, web.Response): + return data + + width: int | str | None = request.query.get("width") + height: int | str | None = request.query.get("height") + + if width is not None: + try: + width = int(width) + except ValueError: + return _400("Invalid width param") + if height is not None: + try: + height = int(height) + except ValueError: + return _400("Invalid height param") + + try: + thumbnail = await data.api.get_event_thumbnail( + event_id, width=width, height=height + ) + except ClientError as err: + return _404(err) + + if thumbnail is None: + return _404("Event thumbnail not found") + + return web.Response(body=thumbnail, content_type="image/jpeg") + + +class VideoProxyView(ProtectProxyView): + """View to proxy video clips from UniFi Protect.""" + + url = "/api/unifiprotect/video/{nvr_id}/{camera_id}/{start}/{end}" + name = "api:unifiprotect_thumbnail" + + async def get( + self, request: web.Request, nvr_id: str, camera_id: str, start: str, end: str + ) -> web.StreamResponse: + """Get Camera Video clip.""" + + data = self._get_data_or_404(nvr_id) + if isinstance(data, web.Response): + return data + + camera = data.api.bootstrap.cameras.get(camera_id) + if camera is None: + return _404(f"Invalid camera ID: {camera_id}") + if not camera.can_read_media(data.api.bootstrap.auth_user): + return _403(f"User cannot read media from camera: {camera.id}") + + try: + start_dt = datetime.fromisoformat(start) + except ValueError: + return _400("Invalid start") + + try: + end_dt = datetime.fromisoformat(end) + except ValueError: + return _400("Invalid end") + + response = web.StreamResponse( + status=200, + reason="OK", + headers={ + "Content-Type": "video/mp4", + }, + ) + + async def iterator(total: int, chunk: bytes | None) -> None: + if not response.prepared: + response.content_length = total + await response.prepare(request) + + if chunk is not None: + await response.write(chunk) + + try: + await camera.get_video(start_dt, end_dt, iterator_callback=iterator) + except ClientError as err: + return _404(err) + + if response.prepared: + await response.write_eof() + return response diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index e4e1900d8f2..5e03df71750 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb} - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) def _element_changed(element, changeset): if (change := changeset.get("last_change")) is None: @@ -75,11 +75,6 @@ class UpbEntity(Entity): element_type = "link" if element.addr.is_link else "device" self._unique_id = f"{unique_id}_{element_type}_{element.addr}" - @property - def name(self): - """Name of the element.""" - return self._element.name - @property def unique_id(self): """Return unique id of the element.""" diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 13f680d9e5a..98a775c18ab 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -49,6 +49,8 @@ async def async_setup_entry( class UpbLight(UpbAttachedEntity, LightEntity): """Representation of an UPB Light.""" + _attr_has_entity_name = True + def __init__(self, element, unique_id, upb): """Initialize an UpbLight.""" super().__init__(element, unique_id, upb) diff --git a/homeassistant/components/upb/scene.py b/homeassistant/components/upb/scene.py index 2334e87aeaa..fe6f07199c4 100644 --- a/homeassistant/components/upb/scene.py +++ b/homeassistant/components/upb/scene.py @@ -49,6 +49,11 @@ async def async_setup_entry( class UpbLink(UpbEntity, Scene): """Representation of an UPB Link.""" + def __init__(self, element, unique_id, upb): + """Initialize the base of all UPB devices.""" + super().__init__(element, unique_id, upb) + self._attr_name = element.name + async def async_activate(self, **kwargs: Any) -> None: """Activate the task.""" self._element.activate() diff --git a/homeassistant/components/upb/translations/pt.json b/homeassistant/components/upb/translations/pt.json index 657ce03e544..3bd5d8358a6 100644 --- a/homeassistant/components/upb/translations/pt.json +++ b/homeassistant/components/upb/translations/pt.json @@ -6,6 +6,13 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "protocol": "Protocolo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index e42659948d8..cd356925de1 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_UPCLOUD].coordinators[entry.data[CONF_USERNAME]] = coordinator # Forward entry setup - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 27d69f9c509..a45e58f28bc 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -167,7 +167,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Setup platforms, creating sensors/binary_sensors. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/upnp/translations/pt.json b/homeassistant/components/upnp/translations/pt.json index 022d1c823c1..6fa823925e1 100644 --- a/homeassistant/components/upnp/translations/pt.json +++ b/homeassistant/components/upnp/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "UPnP/IGD j\u00e1 est\u00e1 configurado", - "no_devices_found": "Nenhum dispositivo UPnP / IGD encontrado na rede." + "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "error": { "one": "um", diff --git a/homeassistant/components/uptime/__init__.py b/homeassistant/components/uptime/__init__.py index 1c36fea3b32..b2f912751f7 100644 --- a/homeassistant/components/uptime/__init__.py +++ b/homeassistant/components/uptime/__init__.py @@ -7,7 +7,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 944f9b77de8..3f7b7f5da25 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -12,6 +12,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -58,10 +60,15 @@ class UptimeSensor(SensorEntity): """Representation of an uptime sensor.""" _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_has_entity_name = True _attr_should_poll = False def __init__(self, entry: ConfigEntry) -> None: """Initialize the uptime sensor.""" - self._attr_name = entry.title self._attr_native_value = dt_util.utcnow() self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + name=entry.title, + identifiers={(DOMAIN, entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/homeassistant/components/uptime/translations/ja.json b/homeassistant/components/uptime/translations/ja.json index 99dc644a0e5..8614c751069 100644 --- a/homeassistant/components/uptime/translations/ja.json +++ b/homeassistant/components/uptime/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index a4c975ff58e..14221463c0e 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/uptimerobot/translations/hu.json b/homeassistant/components/uptimerobot/translations/hu.json index d17c9bf8ac1..4a607c03303 100644 --- a/homeassistant/components/uptimerobot/translations/hu.json +++ b/homeassistant/components/uptimerobot/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt", "unknown": "V\u00e1ratlan hiba" }, diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index e8c66b5088b..55ff808725f 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -19,7 +19,7 @@ "api_key": "API\u30ad\u30fc" }, "description": "UptimeRobot\u304b\u3089\u65b0\u898f\u306e\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/uptimerobot/translations/pt.json b/homeassistant/components/uptimerobot/translations/pt.json index 10c16aafa0f..826cdc5aa59 100644 --- a/homeassistant/components/uptimerobot/translations/pt.json +++ b/homeassistant/components/uptimerobot/translations/pt.json @@ -4,7 +4,20 @@ "unknown": "Erro inesperado" }, "error": { - "invalid_api_key": "Chave de API inv\u00e1lida" + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "api_key": "Chave da API" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 7a56a659d07..5783401df13 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -1,12 +1,13 @@ """The USB Discovery integration.""" from __future__ import annotations +from collections.abc import Coroutine import dataclasses import fnmatch import logging import os import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -109,7 +110,7 @@ class USBDiscovery: self.usb = usb self.seen: set[tuple[str, ...]] = set() self.observer_active = False - self._request_debouncer: Debouncer | None = None + self._request_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None async def async_setup(self) -> None: """Set up USB Discovery.""" diff --git a/homeassistant/components/usb/manifest.json b/homeassistant/components/usb/manifest.json index 87b81965c25..22dca558379 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -2,7 +2,7 @@ "domain": "usb", "name": "USB Discovery", "documentation": "https://www.home-assistant.io/integrations/usb", - "requirements": ["pyudev==0.22.0", "pyserial==3.5"], + "requirements": ["pyudev==0.23.2", "pyserial==3.5"], "codeowners": ["@bdraco"], "dependencies": ["websocket_api"], "quality_scale": "internal", diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index 0680848f70a..882dc588eba 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -3,6 +3,7 @@ "name": "U.S. Citizenship and Immigration Services (USCIS)", "documentation": "https://www.home-assistant.io/integrations/uscis", "requirements": ["uscisstatus==0.1.1"], + "dependencies": ["repairs"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["uscisstatus"] diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index 564c449e8b6..b4719243a8b 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -7,6 +7,7 @@ import logging import uscisstatus import voluptuous as vol +from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -34,6 +35,18 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the platform in Home Assistant and Case Information.""" + create_issue( + hass, + "uscis", + "pending_removal", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The USCIS sensor component is deprecated and will be removed in Home Assistant 2022.10" + ) uscis = UscisSensor(config["case_id"], config[CONF_NAME]) uscis.update() if uscis.valid_case_id: diff --git a/homeassistant/components/uscis/strings.json b/homeassistant/components/uscis/strings.json new file mode 100644 index 00000000000..b8dec86db18 --- /dev/null +++ b/homeassistant/components/uscis/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The USCIS integration is being removed", + "description": "The U.S. Citizenship and Immigration Services (USCIS) integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/uscis/translations/ca.json b/homeassistant/components/uscis/translations/ca.json new file mode 100644 index 00000000000..2072cfb0d92 --- /dev/null +++ b/homeassistant/components/uscis/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3 de Serveis de Ciutadania i Immigraci\u00f3 dels Estats Units (USCIS) est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 es basa en el 'webscraping', que no est\u00e0 adm\u00e8s. \n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", + "title": "S'est\u00e0 eliminant la integraci\u00f3 USCIS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/de.json b/homeassistant/components/uscis/translations/de.json new file mode 100644 index 00000000000..273a340d892 --- /dev/null +++ b/homeassistant/components/uscis/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die Integration der U.S. Citizenship and Immigration Services (USCIS) wird aus Home Assistant entfernt und steht ab Home Assistant 2022.10 nicht mehr zur Verf\u00fcgung.\n\nDie Integration wird entfernt, weil sie auf Webscraping beruht, was nicht erlaubt ist.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die USCIS-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/el.json b/homeassistant/components/uscis/translations/el.json new file mode 100644 index 00000000000..d89ae50773a --- /dev/null +++ b/homeassistant/components/uscis/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03a5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u0399\u03b8\u03b1\u03b3\u03ad\u03bd\u03b5\u03b9\u03b1\u03c2 \u03ba\u03b1\u03b9 \u039c\u03b5\u03c4\u03b1\u03bd\u03ac\u03c3\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03c9\u03bd \u0397\u03a0\u0391 (USCIS) \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03c0\u03c1\u03bf\u03c2 \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10.\n\n\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 webscraping, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9.\n\n\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 USCIS \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/en.json b/homeassistant/components/uscis/translations/en.json new file mode 100644 index 00000000000..24e7e9ceea0 --- /dev/null +++ b/homeassistant/components/uscis/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The U.S. Citizenship and Immigration Services (USCIS) integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The USCIS integration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/et.json b/homeassistant/components/uscis/translations/et.json new file mode 100644 index 00000000000..0dc9325b715 --- /dev/null +++ b/homeassistant/components/uscis/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "USA kodakondsus- ja immigratsiooniteenistuse (USCIS) integratsioon ootab eemaldamist Home Assistantist ja ei ole enam k\u00e4ttesaadav alates Home Assistant 2022.10.\n\nIntegratsioon eemaldatakse, sest see p\u00f5hineb veebiotsingul, mis ei ole lubatud.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "USCIS-i sidumine eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/fr.json b/homeassistant/components/uscis/translations/fr.json new file mode 100644 index 00000000000..8d9e1c3f8ba --- /dev/null +++ b/homeassistant/components/uscis/translations/fr.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "L'int\u00e9gration USCIS est en cours de suppression" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/hu.json b/homeassistant/components/uscis/translations/hu.json new file mode 100644 index 00000000000..181cbaf076f --- /dev/null +++ b/homeassistant/components/uscis/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A U.S. Citizenship and Immigration Services (USCIS) integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a Home Assistant 2022.10-t\u0151l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sa az\u00e9rt t\u00f6rt\u00e9nik, mert webscrapingre t\u00e1maszkodik, ami nem megengedett.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az USCIS integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/id.json b/homeassistant/components/uscis/translations/id.json new file mode 100644 index 00000000000..37e7278a916 --- /dev/null +++ b/homeassistant/components/uscis/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasi Layanan Kewarganegaraan dan Imigrasi AS (USCIS) sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nIntegrasi ini dalam proses penghapusan, karena bergantung pada proses webscraping, yang tidak diizinkan.\n\nHapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi USCIS dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/it.json b/homeassistant/components/uscis/translations/it.json new file mode 100644 index 00000000000..1cb9e54a6b7 --- /dev/null +++ b/homeassistant/components/uscis/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione U.S. Citizenship and Immigration Services (USCIS) \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione sar\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "L'integrazione USCIS verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ja.json b/homeassistant/components/uscis/translations/ja.json new file mode 100644 index 00000000000..b5abb7e0825 --- /dev/null +++ b/homeassistant/components/uscis/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "USCIS\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/pl.json b/homeassistant/components/uscis/translations/pl.json new file mode 100644 index 00000000000..82e02996f7c --- /dev/null +++ b/homeassistant/components/uscis/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja US Citizenship and Immigration Services (USCIS) oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c opiera si\u0119 na webscrapingu, co jest niedozwolone. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Trwa usuwanie integracji USCIS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/pt-BR.json b/homeassistant/components/uscis/translations/pt-BR.json new file mode 100644 index 00000000000..76182bfa2d2 --- /dev/null +++ b/homeassistant/components/uscis/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o dos Servi\u00e7os de Cidadania e Imigra\u00e7\u00e3o dos EUA (USCIS) est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, pois depende de webscraping, o que n\u00e3o \u00e9 permitido. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do USCIS est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ru.json b/homeassistant/components/uscis/translations/ru.json new file mode 100644 index 00000000000..f3b70020245 --- /dev/null +++ b/homeassistant/components/uscis/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0421\u043b\u0443\u0436\u0431\u044b \u0433\u0440\u0430\u0436\u0434\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u0438\u043c\u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0421\u0428\u0410 (USCIS) \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f USCIS \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/zh-Hant.json b/homeassistant/components/uscis/translations/zh-Hant.json new file mode 100644 index 00000000000..ccc72d3d1dd --- /dev/null +++ b/homeassistant/components/uscis/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc\u4f7f\u7528\u4e86\u4e0d\u88ab\u5141\u8a31\u7684\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "USCIS \u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 6e3eb9b2337..c82705174fe 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -1,15 +1,19 @@ """Support for U.S. Geological Survey Earthquake Hazards Program Feeds.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import logging +from typing import Any from aio_geojson_usgs_earthquakes import UsgsEarthquakeHazardsProgramFeedManager +from aio_geojson_usgs_earthquakes.feed_entry import ( + UsgsEarthquakeHazardsProgramFeedEntry, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_TIME, CONF_LATITUDE, CONF_LONGITUDE, @@ -96,14 +100,14 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the USGS Earthquake Hazards Program Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - feed_type = config[CONF_FEED_TYPE] - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + feed_type: str = config[CONF_FEED_TYPE] + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE] + radius_in_km: float = config[CONF_RADIUS] + minimum_magnitude: float = config[CONF_MINIMUM_MAGNITUDE] # Initialize the entity manager. manager = UsgsEarthquakesFeedEntityManager( hass, @@ -128,14 +132,14 @@ class UsgsEarthquakesFeedEntityManager: def __init__( self, - hass, - async_add_entities, - scan_interval, - coordinates, - feed_type, - radius_in_km, - minimum_magnitude, - ): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + feed_type: str, + radius_in_km: float, + minimum_magnitude: float, + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass @@ -153,10 +157,10 @@ class UsgsEarthquakesFeedEntityManager: self._async_add_entities = async_add_entities self._scan_interval = scan_interval - async def async_init(self): + async def async_init(self) -> None: """Schedule initial and regular updates based on configured time interval.""" - async def update(event_time): + async def update(event_time: datetime) -> None: """Update.""" await self.async_update() @@ -164,26 +168,28 @@ class UsgsEarthquakesFeedEntityManager: async_track_time_interval(self._hass, update, self._scan_interval) _LOGGER.debug("Feed entity manager initialized") - async def async_update(self): + async def async_update(self) -> None: """Refresh data.""" await self._feed_manager.update() _LOGGER.debug("Feed entity manager updated") - def get_entry(self, external_id): + def get_entry( + self, external_id: str + ) -> UsgsEarthquakeHazardsProgramFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - async def _generate_entity(self, external_id): + async def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = UsgsEarthquakesEvent(self, external_id) # Add new entities to HA. self._async_add_entities([new_entity], True) - async def _update_entity(self, external_id): + async def _update_entity(self, external_id: str) -> None: """Update entity.""" async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - async def _remove_entity(self, external_id): + async def _remove_entity(self, external_id: str) -> None: """Remove entity.""" async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) @@ -191,15 +197,17 @@ class UsgsEarthquakesFeedEntityManager: class UsgsEarthquakesEvent(GeolocationEvent): """This represents an external event with USGS Earthquake data.""" - def __init__(self, feed_manager, external_id): + _attr_icon = "mdi:pulse" + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = DEFAULT_UNIT_OF_MEASUREMENT + + def __init__( + self, feed_manager: UsgsEarthquakesFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._place = None self._magnitude = None self._time = None @@ -207,10 +215,10 @@ class UsgsEarthquakesEvent(GeolocationEvent): self._status = None self._type = None self._alert = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -224,36 +232,33 @@ class UsgsEarthquakesEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for USGS Earthquake events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed( + self, feed_entry: UsgsEarthquakeHazardsProgramFeedEntry + ) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._place = feed_entry.place self._magnitude = feed_entry.magnitude self._time = feed_entry.time @@ -263,42 +268,7 @@ class UsgsEarthquakesEvent(GeolocationEvent): self._alert = feed_entry.alert @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return DEFAULT_UNIT_OF_MEASUREMENT - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( @@ -310,7 +280,6 @@ class UsgsEarthquakesEvent(GeolocationEvent): (ATTR_STATUS, self._status), (ATTR_TYPE, self._type), (ATTR_ALERT, self._alert), - (ATTR_ATTRIBUTION, self._attribution), ): if value or isinstance(value, bool): attributes[key] = value diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 05ed01f8208..b17592cdf0b 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -200,7 +200,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.options.get(CONF_TARIFFS): # Only a single meter sensor is required hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] = None - hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) else: # Create tariff selection + one meter sensor for each tariff entity_entry = entity_registry.async_get_or_create( @@ -209,7 +209,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_UTILITY][entry.entry_id][ CONF_TARIFF_ENTITY ] = entity_entry.entity_id - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, (Platform.SELECT, Platform.SENSOR) ) diff --git a/homeassistant/components/utility_meter/translations/pt.json b/homeassistant/components/utility_meter/translations/pt.json new file mode 100644 index 00000000000..1ad401c42e6 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "tariffs": "Tarifas suportadas" + }, + "description": "Criar um sensor que monitorize o consumo de v\u00e1rias utilidades (por exemplo, energia, g\u00e1s, \u00e1gua, aquecimento) durante um per\u00edodo configurado, tipicamente mensal. O sensor de contador de utilidades (utility_meter) suporta opcionalmente a divis\u00e3o do consumo por tarifas; nesse caso \u00e9 criado um sensor para cada tarifa, bem como uma entidade selecionada para escolher a tarifa atual." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/sv.json b/homeassistant/components/utility_meter/translations/sv.json index 38d433e7b42..6ef91919b73 100644 --- a/homeassistant/components/utility_meter/translations/sv.json +++ b/homeassistant/components/utility_meter/translations/sv.json @@ -1,3 +1,35 @@ { - "title": "Tj\u00e4nsten \u00e4r redan konfigurerad" + "config": { + "step": { + "user": { + "data": { + "cycle": "\u00c5terst\u00e4ll m\u00e4tarcykel", + "delta_values": "Delta-v\u00e4rden", + "name": "Namn", + "net_consumption": "Nettof\u00f6rbrukning", + "offset": "\u00c5terst\u00e4ll m\u00e4taroffset", + "source": "Sensorsk\u00e4lla", + "tariffs": "Tariffer som st\u00f6ds" + }, + "data_description": { + "delta_values": "Aktivera om k\u00e4llv\u00e4rdena \u00e4r deltav\u00e4rden sedan den senaste avl\u00e4sningen ist\u00e4llet f\u00f6r absoluta v\u00e4rden.", + "net_consumption": "Aktivera om k\u00e4llan \u00e4r en nettom\u00e4tare, vilket betyder att den b\u00e5de kan \u00f6ka och minska.", + "offset": "F\u00f6rskjut dagen f\u00f6r en m\u00e5natlig m\u00e4tar\u00e5terst\u00e4llning.", + "tariffs": "En lista \u00f6ver st\u00f6dda tariffer, l\u00e4mna tom om bara en enda tariff beh\u00f6vs." + }, + "description": "Skapa en sensor som \u00f6vervakar f\u00f6rbrukningen av t.ex. energi, gas, vatten, v\u00e4rme, \u00f6ver en konfigurerad tidsperiod, vanligtvis m\u00e5nadsvis. M\u00e4tarsensorn st\u00f6djer valfritt att dela upp f\u00f6rbrukningen efter tariffer, i s\u00e5 fall skapas en sensor f\u00f6r varje tariff samt en val entitet f\u00f6r att v\u00e4lja den aktuella tariffen.", + "title": "L\u00e4gg till m\u00e4tare" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sensork\u00e4lla" + } + } + } + }, + "title": "M\u00e4tare" } \ No newline at end of file diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index a69542596c8..6a2234af9e6 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -219,7 +219,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "name": name, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vallox/binary_sensor.py b/homeassistant/components/vallox/binary_sensor.py index 762b63c0c1d..9f1b3018186 100644 --- a/homeassistant/components/vallox/binary_sensor.py +++ b/homeassistant/components/vallox/binary_sensor.py @@ -21,6 +21,7 @@ class ValloxBinarySensor(ValloxEntity, BinarySensorEntity): entity_description: ValloxBinarySensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True def __init__( self, @@ -33,7 +34,6 @@ class ValloxBinarySensor(ValloxEntity, BinarySensorEntity): self.entity_description = description - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{self._device_uuid}-{description.key}" @property @@ -59,7 +59,7 @@ class ValloxBinarySensorEntityDescription( SENSORS: tuple[ValloxBinarySensorEntityDescription, ...] = ( ValloxBinarySensorEntityDescription( key="post_heater", - name="Post Heater", + name="Post heater", icon="mdi:radiator", metric_key="A_CYC_IO_HEATER", ), diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 4ba7d2d88fd..be496bbf899 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -83,6 +83,7 @@ class ValloxFan(ValloxEntity, FanEntity): """Representation of the fan.""" _attr_supported_features = FanEntityFeature.PRESET_MODE + _attr_has_entity_name = True def __init__( self, @@ -95,7 +96,6 @@ class ValloxFan(ValloxEntity, FanEntity): self._client = client - self._attr_name = name self._attr_unique_id = str(self._device_uuid) @property diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 71b0750e2f2..5c862562fc1 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -2,7 +2,7 @@ "domain": "vallox", "name": "Vallox", "documentation": "https://www.home-assistant.io/integrations/vallox", - "requirements": ["vallox-websocket-api==2.11.0"], + "requirements": ["vallox-websocket-api==2.12.0"], "codeowners": ["@andre-richter", "@slovdahl", "@viiru-"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 54a010c3e3d..d3357e50ad2 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, + FREQUENCY_HERTZ, PERCENTAGE, TEMP_CELSIUS, ) @@ -32,11 +33,12 @@ from .const import ( ) -class ValloxSensor(ValloxEntity, SensorEntity): +class ValloxSensorEntity(ValloxEntity, SensorEntity): """Representation of a Vallox sensor.""" entity_description: ValloxSensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True def __init__( self, @@ -49,7 +51,6 @@ class ValloxSensor(ValloxEntity, SensorEntity): self.entity_description = description - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{self._device_uuid}-{description.key}" @property @@ -58,10 +59,17 @@ class ValloxSensor(ValloxEntity, SensorEntity): if (metric_key := self.entity_description.metric_key) is None: return None - return self.coordinator.data.get_metric(metric_key) + value = self.coordinator.data.get_metric(metric_key) + + if self.entity_description.round_ndigits is not None and isinstance( + value, float + ): + value = round(value, self.entity_description.round_ndigits) + + return value -class ValloxProfileSensor(ValloxSensor): +class ValloxProfileSensor(ValloxSensorEntity): """Child class for profile reporting.""" @property @@ -77,7 +85,7 @@ class ValloxProfileSensor(ValloxSensor): # # Therefore, first query the overall state of the device, and report zero percent fan speed in case # it is not in regular operation mode. -class ValloxFanSpeedSensor(ValloxSensor): +class ValloxFanSpeedSensor(ValloxSensorEntity): """Child class for fan speed reporting.""" @property @@ -87,7 +95,7 @@ class ValloxFanSpeedSensor(ValloxSensor): return super().native_value if fan_is_on else 0 -class ValloxFilterRemainingSensor(ValloxSensor): +class ValloxFilterRemainingSensor(ValloxSensorEntity): """Child class for filter remaining time reporting.""" @property @@ -104,7 +112,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): ) -class ValloxCellStateSensor(ValloxSensor): +class ValloxCellStateSensor(ValloxSensorEntity): """Child class for cell state reporting.""" @property @@ -123,41 +131,62 @@ class ValloxSensorEntityDescription(SensorEntityDescription): """Describes Vallox sensor entity.""" metric_key: str | None = None - sensor_type: type[ValloxSensor] = ValloxSensor + entity_type: type[ValloxSensorEntity] = ValloxSensorEntity + round_ndigits: int | None = None -SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( +SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ValloxSensorEntityDescription( key="current_profile", - name="Current Profile", + name="Current profile", icon="mdi:gauge", - sensor_type=ValloxProfileSensor, + entity_type=ValloxProfileSensor, ), ValloxSensorEntityDescription( key="fan_speed", - name="Fan Speed", + name="Fan speed", metric_key="A_CYC_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, - sensor_type=ValloxFanSpeedSensor, + entity_type=ValloxFanSpeedSensor, + ), + ValloxSensorEntityDescription( + key="extract_fan_speed", + name="Extract fan speed", + metric_key="A_CYC_EXTR_FAN_SPEED", + icon="mdi:fan", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=FREQUENCY_HERTZ, + entity_type=ValloxFanSpeedSensor, + entity_registry_enabled_default=False, + ), + ValloxSensorEntityDescription( + key="supply_fan_speed", + name="Supply fan speed", + metric_key="A_CYC_SUPP_FAN_SPEED", + icon="mdi:fan", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=FREQUENCY_HERTZ, + entity_type=ValloxFanSpeedSensor, + entity_registry_enabled_default=False, ), ValloxSensorEntityDescription( key="remaining_time_for_filter", - name="Remaining Time For Filter", + name="Remaining time for filter", device_class=SensorDeviceClass.TIMESTAMP, - sensor_type=ValloxFilterRemainingSensor, + entity_type=ValloxFilterRemainingSensor, ), ValloxSensorEntityDescription( key="cell_state", - name="Cell State", + name="Cell state", icon="mdi:swap-horizontal-bold", metric_key="A_CYC_CELL_STATE", - sensor_type=ValloxCellStateSensor, + entity_type=ValloxCellStateSensor, ), ValloxSensorEntityDescription( key="extract_air", - name="Extract Air", + name="Extract air", metric_key="A_CYC_TEMP_EXTRACT_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -165,7 +194,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="exhaust_air", - name="Exhaust Air", + name="Exhaust air", metric_key="A_CYC_TEMP_EXHAUST_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -173,7 +202,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="outdoor_air", - name="Outdoor Air", + name="Outdoor air", metric_key="A_CYC_TEMP_OUTDOOR_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -181,12 +210,29 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="supply_air", - name="Supply Air", + name="Supply air", metric_key="A_CYC_TEMP_SUPPLY_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), + ValloxSensorEntityDescription( + key="supply_cell_air", + name="Supply cell air", + metric_key="A_CYC_TEMP_SUPPLY_CELL_AIR", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + ValloxSensorEntityDescription( + key="optional_air", + name="Optional air", + metric_key="A_CYC_TEMP_OPTIONAL", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + entity_registry_enabled_default=False, + ), ValloxSensorEntityDescription( key="humidity", name="Humidity", @@ -202,6 +248,8 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + round_ndigits=0, ), ValloxSensorEntityDescription( key="co2", @@ -210,6 +258,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + entity_registry_enabled_default=False, ), ) @@ -223,7 +272,7 @@ async def async_setup_entry( async_add_entities( [ - description.sensor_type(name, coordinator, description) - for description in SENSORS + description.entity_type(name, coordinator, description) + for description in SENSOR_ENTITIES ] ) diff --git a/homeassistant/components/vallox/translations/pt.json b/homeassistant/components/vallox/translations/pt.json index 0c5c7760566..f5c3b1c7b61 100644 --- a/homeassistant/components/vallox/translations/pt.json +++ b/homeassistant/components/vallox/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + }, "error": { "unknown": "Erro inesperado" } diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 9b5a52306d8..eeeee2f9716 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _migrate_device_identifiers(hass, entry.entry_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.services.has_service(DOMAIN, SERVICE_SCAN): return True diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 759908c87e4..b543f6d947a 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: await venstar_data_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[config.entry_id] = venstar_data_coordinator - hass.config_entries.async_setup_platforms(config, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config, PLATFORMS) return True diff --git a/homeassistant/components/venstar/translations/pt.json b/homeassistant/components/venstar/translations/pt.json new file mode 100644 index 00000000000..04374af8e82 --- /dev/null +++ b/homeassistant/components/venstar/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 2dda8fdb7c4..a63e63d74c0 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -142,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: set_controller_data(hass, entry, controller_data) # Forward the config data to the necessary platforms. - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, platforms=get_configured_platforms(controller_data) ) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index d42a6ad004b..9ad8db08d59 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -3,9 +3,10 @@ from __future__ import annotations from contextlib import suppress import os +from pathlib import Path from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.const import CONF_EMAIL, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv @@ -28,6 +29,8 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Verisure from a config entry.""" + await hass.async_add_executor_job(migrate_cookie_files, hass, entry) + coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) if not await coordinator.async_login(): @@ -43,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -64,3 +67,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN] return True + + +def migrate_cookie_files(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Migrate old cookie file to new location.""" + cookie_file = Path(hass.config.path(STORAGE_DIR, f"verisure_{entry.unique_id}")) + if cookie_file.exists(): + cookie_file.rename( + hass.config.path(STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}") + ) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 8f92ee0a5dd..5030e01c8b1 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -33,7 +33,7 @@ class VerisureAlarm( """Representation of a Verisure alarm status.""" _attr_code_format = CodeFormat.NUMBER - _attr_name = "Verisure Alarm" + _attr_has_entity_name = True _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 217890b8a01..45e29cfa5f1 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -39,13 +39,13 @@ class VerisureDoorWindowSensor( """Representation of a Verisure door window sensor.""" _attr_device_class = BinarySensorDeviceClass.OPENING + _attr_has_entity_name = True def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure door window sensor.""" super().__init__(coordinator) - self._attr_name = coordinator.data["door_window"][serial_number]["area"] self._attr_unique_id = f"{serial_number}_door_window" self.serial_number = serial_number @@ -84,9 +84,10 @@ class VerisureEthernetStatus( ): """Representation of a Verisure VBOX internet status.""" - _attr_name = "Verisure Ethernet status" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + _attr_name = "Ethernet status" @property def unique_id(self) -> str: diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index c753bf2c5dc..98ed41c5b9f 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -46,6 +46,8 @@ async def async_setup_entry( class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera): """Representation of a Verisure camera.""" + _attr_has_entity_name = True + def __init__( self, coordinator: VerisureDataUpdateCoordinator, @@ -56,7 +58,6 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera) super().__init__(coordinator) Camera.__init__(self) - self._attr_name = coordinator.data["cameras"][serial_number]["area"] self._attr_unique_id = serial_number self.serial_number = serial_number diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 41687dbc6a4..d53c7c9ed66 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -13,9 +13,10 @@ from verisure import ( import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.storage import STORAGE_DIR from .const import ( CONF_GIID, @@ -34,8 +35,8 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): email: str entry: ConfigEntry - installations: dict[str, str] password: str + verisure: Verisure @staticmethod @callback @@ -50,25 +51,41 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - verisure = Verisure( - username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + self.email = user_input[CONF_EMAIL] + self.password = user_input[CONF_PASSWORD] + self.verisure = Verisure( + username=self.email, + password=self.password, + cookieFileName=self.hass.config.path( + STORAGE_DIR, f"verisure_{user_input[CONF_EMAIL]}" + ), ) + try: - await self.hass.async_add_executor_job(verisure.login) + await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: - LOGGER.debug("Could not log in to Verisure, %s", ex) - errors["base"] = "invalid_auth" + if "Multifactor authentication enabled" in str(ex): + try: + await self.hass.async_add_executor_job(self.verisure.login_mfa) + except ( + VerisureLoginError, + VerisureError, + VerisureResponseError, + ) as mfa_ex: + LOGGER.debug( + "Unexpected response from Verisure during MFA set up, %s", + mfa_ex, + ) + errors["base"] = "unknown_mfa" + else: + return await self.async_step_mfa() + else: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" except (VerisureError, VerisureResponseError) as ex: LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" else: - self.email = user_input[CONF_EMAIL] - self.password = user_input[CONF_PASSWORD] - self.installations = { - inst["giid"]: f"{inst['alias']} ({inst['street']})" - for inst in verisure.installations - } - return await self.async_step_installation() return self.async_show_form( @@ -82,26 +99,63 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle multifactor authentication step.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + self.verisure.mfa_validate, user_input[CONF_CODE], True + ) + await self.hass.async_add_executor_job(self.verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + return await self.async_step_installation() + + return self.async_show_form( + step_id="mfa", + data_schema=vol.Schema( + { + vol.Required(CONF_CODE): vol.All( + vol.Coerce(str), vol.Length(min=6, max=6) + ) + } + ), + errors=errors, + ) + async def async_step_installation( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Select Verisure installation to add.""" - if len(self.installations) == 1: - user_input = {CONF_GIID: list(self.installations)[0]} + installations = { + inst["giid"]: f"{inst['alias']} ({inst['street']})" + for inst in self.verisure.installations or [] + } if user_input is None: - return self.async_show_form( - step_id="installation", - data_schema=vol.Schema( - {vol.Required(CONF_GIID): vol.In(self.installations)} - ), - ) + if len(installations) != 1: + return self.async_show_form( + step_id="installation", + data_schema=vol.Schema( + {vol.Required(CONF_GIID): vol.In(installations)} + ), + ) + user_input = {CONF_GIID: list(installations)[0]} await self.async_set_unique_id(user_input[CONF_GIID]) self._abort_if_unique_id_configured() return self.async_create_entry( - title=self.installations[user_input[CONF_GIID]], + title=installations[user_input[CONF_GIID]], data={ CONF_EMAIL: self.email, CONF_PASSWORD: self.password, @@ -124,14 +178,38 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - verisure = Verisure( - username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + self.email = user_input[CONF_EMAIL] + self.password = user_input[CONF_PASSWORD] + + self.verisure = Verisure( + username=self.email, + password=self.password, + cookieFileName=self.hass.config.path( + STORAGE_DIR, f"verisure-{user_input[CONF_EMAIL]}" + ), ) + try: - await self.hass.async_add_executor_job(verisure.login) + await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: - LOGGER.debug("Could not log in to Verisure, %s", ex) - errors["base"] = "invalid_auth" + if "Multifactor authentication enabled" in str(ex): + try: + await self.hass.async_add_executor_job(self.verisure.login_mfa) + except ( + VerisureLoginError, + VerisureError, + VerisureResponseError, + ) as mfa_ex: + LOGGER.debug( + "Unexpected response from Verisure during MFA set up, %s", + mfa_ex, + ) + errors["base"] = "unknown_mfa" + else: + return await self.async_step_reauth_mfa() + else: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" except (VerisureError, VerisureResponseError) as ex: LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" @@ -161,6 +239,51 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle multifactor authentication step during re-authentication.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + self.verisure.mfa_validate, user_input[CONF_CODE], True + ) + await self.hass.async_add_executor_job(self.verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_EMAIL: self.email, + CONF_PASSWORD: self.password, + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_mfa", + data_schema=vol.Schema( + { + vol.Required(CONF_CODE): vol.All( + vol.Coerce(str), + vol.Length(min=6, max=6), + ) + } + ), + errors=errors, + ) + class VerisureOptionsFlowHandler(OptionsFlow): """Handle Verisure options.""" diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index 821e2830339..17cadb9598f 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -31,7 +31,9 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): self.verisure = Verisure( username=entry.data[CONF_EMAIL], password=entry.data[CONF_PASSWORD], - cookieFileName=hass.config.path(STORAGE_DIR, f"verisure_{entry.entry_id}"), + cookieFileName=hass.config.path( + STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}" + ), ) super().__init__( diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 8074cf28f32..02cdad158ca 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -59,13 +59,13 @@ async def async_setup_entry( class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEntity): """Representation of a Verisure doorlock.""" + _attr_has_entity_name = True + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure lock.""" super().__init__(coordinator) - - self._attr_name = coordinator.data["locks"][serial_number]["area"] self._attr_unique_id = serial_number self.serial_number = serial_number diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index c71be7ee4fc..820b8a20f14 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -2,7 +2,7 @@ "domain": "verisure", "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", - "requirements": ["vsure==1.7.3"], + "requirements": ["vsure==1.8.1"], "codeowners": ["@frenck"], "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 3b8f722c6f7..676082e4cda 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -51,6 +51,8 @@ class VerisureThermometer( """Representation of a Verisure thermometer.""" _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_has_entity_name = True + _attr_name = "Temperature" _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_state_class = SensorStateClass.MEASUREMENT @@ -62,12 +64,6 @@ class VerisureThermometer( self._attr_unique_id = f"{serial_number}_temperature" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of the entity.""" - name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] - return f"{name} Temperature" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -106,6 +102,8 @@ class VerisureHygrometer( """Representation of a Verisure hygrometer.""" _attr_device_class = SensorDeviceClass.HUMIDITY + _attr_has_entity_name = True + _attr_name = "Humidity" _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = SensorStateClass.MEASUREMENT @@ -117,12 +115,6 @@ class VerisureHygrometer( self._attr_unique_id = f"{serial_number}_humidity" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of the entity.""" - name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] - return f"{name} Humidity" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -160,6 +152,8 @@ class VerisureMouseDetection( ): """Representation of a Verisure mouse detector.""" + _attr_name = "Mouse" + _attr_has_entity_name = True _attr_native_unit_of_measurement = "Mice" def __init__( @@ -170,12 +164,6 @@ class VerisureMouseDetection( self._attr_unique_id = f"{serial_number}_mice" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of the entity.""" - name = self.coordinator.data["mice"][self.serial_number]["area"] - return f"{name} Mouse" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json index 5170bff5faa..c8326d73756 100644 --- a/homeassistant/components/verisure/strings.json +++ b/homeassistant/components/verisure/strings.json @@ -8,6 +8,12 @@ "password": "[%key:common::config_flow::data::password%]" } }, + "mfa": { + "data": { + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", + "code": "Verification Code" + } + }, "installation": { "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant.", "data": { @@ -20,11 +26,18 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_mfa": { + "data": { + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", + "code": "Verification Code" + } } }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "unknown_mfa": "Unknown error occurred during MFA set up" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 5d1fd728f4a..177beb4272b 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -30,13 +30,13 @@ async def async_setup_entry( class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], SwitchEntity): """Representation of a Verisure smartplug.""" + _attr_has_entity_name = True + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure device.""" super().__init__(coordinator) - - self._attr_name = coordinator.data["smart_plugs"][serial_number]["area"] self._attr_unique_id = serial_number self.serial_number = serial_number diff --git a/homeassistant/components/verisure/translations/bg.json b/homeassistant/components/verisure/translations/bg.json index 0f10e122185..cf602238bf3 100644 --- a/homeassistant/components/verisure/translations/bg.json +++ b/homeassistant/components/verisure/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/verisure/translations/ca.json b/homeassistant/components/verisure/translations/ca.json index 87e441fd937..36c76a15dcd 100644 --- a/homeassistant/components/verisure/translations/ca.json +++ b/homeassistant/components/verisure/translations/ca.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "unknown_mfa": "S'ha produ\u00eft un error desconegut durant la configuraci\u00f3 de l'MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant ha trobat diverses instal\u00b7lacions Verisure al compte de My Pages. Selecciona la instal\u00b7laci\u00f3 a afegir a Home Assistant." }, + "mfa": { + "data": { + "code": "Codi de verificaci\u00f3", + "description": "El teu compte t\u00e9 activada la verificaci\u00f3 en dos passos. Introdueix el codi de verificaci\u00f3 que t'ha enviat Verisure." + } + }, "reauth_confirm": { "data": { "description": "Torna a autenticar-te amb el compte de Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Contrasenya" } }, + "reauth_mfa": { + "data": { + "code": "Codi de verificaci\u00f3", + "description": "El teu compte t\u00e9 activada la verificaci\u00f3 en dos passos. Introdueix el codi de verificaci\u00f3 que t'ha enviat Verisure." + } + }, "user": { "data": { "description": "Inicia sessi\u00f3 amb el compte de Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/de.json b/homeassistant/components/verisure/translations/de.json index 3eaf6ff04f6..b5e544e579b 100644 --- a/homeassistant/components/verisure/translations/de.json +++ b/homeassistant/components/verisure/translations/de.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "unknown_mfa": "Beim MFA-Setup ist ein unbekannter Fehler aufgetreten" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant hat mehrere Verisure-Installationen in deinen My Pages-Konto gefunden. Bitte w\u00e4hle die Installation aus, die du zu Home Assistant hinzuf\u00fcgen m\u00f6chtest." }, + "mfa": { + "data": { + "code": "Verifizierungs-Code", + "description": "Dein Konto hat die 2-Schritt-Verifizierung aktiviert. Bitte gib den Verifizierungscode ein, den du von Verisure erhalten hast." + } + }, "reauth_confirm": { "data": { "description": "Authentifiziere dich erneut mit deinem Verisure My Pages-Konto.", @@ -22,6 +29,12 @@ "password": "Passwort" } }, + "reauth_mfa": { + "data": { + "code": "Verifizierungs-Code", + "description": "Dein Konto hat die 2-Schritt-Verifizierung aktiviert. Bitte gib den Verifizierungscode ein, den du von Verisure erhalten hast." + } + }, "user": { "data": { "description": "Melde dich mit deinen Verisure My Pages-Konto an.", diff --git a/homeassistant/components/verisure/translations/el.json b/homeassistant/components/verisure/translations/el.json index 7d46b4ed96b..fc9e4cc52df 100644 --- a/homeassistant/components/verisure/translations/el.json +++ b/homeassistant/components/verisure/translations/el.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unknown_mfa": "\u0395\u03bc\u03c6\u03b1\u03bd\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "\u03a4\u03bf Home Assistant \u03b2\u03c1\u03ae\u03ba\u03b5 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ad\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 Verisure \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 My Pages. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf Home Assistant." }, + "mfa": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2", + "description": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c3\u03b5 2 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03b7 Verisure." + } + }, "reauth_confirm": { "data": { "description": "\u0395\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" } }, + "reauth_mfa": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2", + "description": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c3\u03b5 2 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03b7 Verisure." + } + }, "user": { "data": { "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/en.json b/homeassistant/components/verisure/translations/en.json index 57f73c3772b..34193ce0d09 100644 --- a/homeassistant/components/verisure/translations/en.json +++ b/homeassistant/components/verisure/translations/en.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unknown_mfa": "Unknown error occurred during MFA set up" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant." }, + "mfa": { + "data": { + "code": "Verification Code", + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you." + } + }, "reauth_confirm": { "data": { "description": "Re-authenticate with your Verisure My Pages account.", @@ -22,6 +29,12 @@ "password": "Password" } }, + "reauth_mfa": { + "data": { + "code": "Verification Code", + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you." + } + }, "user": { "data": { "description": "Sign-in with your Verisure My Pages account.", diff --git a/homeassistant/components/verisure/translations/et.json b/homeassistant/components/verisure/translations/et.json index 78a2c987ef2..25fb59a8aad 100644 --- a/homeassistant/components/verisure/translations/et.json +++ b/homeassistant/components/verisure/translations/et.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Vigane autentimine", - "unknown": "Ootamatu t\u00f5rge" + "unknown": "Ootamatu t\u00f5rge", + "unknown_mfa": "MFA h\u00e4\u00e4lestamisel ilmnes tundmatu t\u00f5rge" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant leidis kontolt Minu lehed mitu Verisure paigaldust. Vali Home Assistantile lisatav paigaldus." }, + "mfa": { + "data": { + "code": "Kinnituskood", + "description": "Kontol on lubatud 2-astmeline kontroll. Palun sisesta Verisure'i poolt saadetud kinnituskood." + } + }, "reauth_confirm": { "data": { "description": "Taastuvasta oma Verisure My Pages'i kontoga.", @@ -22,6 +29,12 @@ "password": "Salas\u00f5na" } }, + "reauth_mfa": { + "data": { + "code": "Kinnituskood", + "description": "Kontol on lubatud 2-astmeline kontroll. Palun sisesta Verisure'i poolt saadetud kinnituskood." + } + }, "user": { "data": { "description": "Logi sisse oma Verisure My Pages kontoga.", diff --git a/homeassistant/components/verisure/translations/fr.json b/homeassistant/components/verisure/translations/fr.json index f3c26abb74a..0848eb6022b 100644 --- a/homeassistant/components/verisure/translations/fr.json +++ b/homeassistant/components/verisure/translations/fr.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Authentification non valide", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "unknown_mfa": "Une erreur inconnue est survenue lors de la configuration de l'authentification multifacteur" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant a trouv\u00e9 plusieurs installations Verisure dans votre compte My Pages. Veuillez s\u00e9lectionner l'installation \u00e0 ajouter \u00e0 Home Assistant." }, + "mfa": { + "data": { + "code": "Code de v\u00e9rification", + "description": "La v\u00e9rification en deux \u00e9tapes est activ\u00e9e sur votre compte. Veuillez saisir le code de v\u00e9rification envoy\u00e9 par Verisure." + } + }, "reauth_confirm": { "data": { "description": "R\u00e9-authentifiez-vous avec votre compte Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Mot de passe" } }, + "reauth_mfa": { + "data": { + "code": "Code de v\u00e9rification", + "description": "La v\u00e9rification en deux \u00e9tapes est activ\u00e9e sur votre compte. Veuillez saisir le code de v\u00e9rification envoy\u00e9 par Verisure." + } + }, "user": { "data": { "description": "Connectez-vous avec votre compte Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/hu.json b/homeassistant/components/verisure/translations/hu.json index 324033b666a..fbb40f2325a 100644 --- a/homeassistant/components/verisure/translations/hu.json +++ b/homeassistant/components/verisure/translations/hu.json @@ -6,14 +6,21 @@ }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unknown_mfa": "Ismeretlen hiba t\u00f6rt\u00e9nt a be\u00e1ll\u00edt\u00e1s sor\u00e1n" }, "step": { "installation": { "data": { "giid": "Telep\u00edt\u00e9s" }, - "description": "Home Assistant t\u00f6bb Verisure telep\u00edt\u00e9st tal\u00e1lt a Saj\u00e1t oldalak fi\u00f3kj\u00e1ban. K\u00e9rj\u00fck, v\u00e1lassza ki azt a telep\u00edt\u00e9st, amelyet hozz\u00e1 k\u00edv\u00e1nja adni a Home Assistant p\u00e9ld\u00e1ny\u00e1hoz." + "description": "Home Assistant t\u00f6bb Verisure telep\u00edt\u00e9st tal\u00e1lt a Saj\u00e1t oldalak fi\u00f3kj\u00e1ban. K\u00e9rem, v\u00e1lassza ki azt a telep\u00edt\u00e9st, amelyet hozz\u00e1 k\u00edv\u00e1nja adni a Home Assistant p\u00e9ld\u00e1ny\u00e1hoz." + }, + "mfa": { + "data": { + "code": "Ellen\u0151rz\u0151 k\u00f3d", + "description": "A fi\u00f3kj\u00e1ban be van \u00e1ll\u00edtva a 2 l\u00e9pcs\u0151s ellen\u0151rz\u00e9s. K\u00e9rem, adja meg a Verisure \u00e1ltal k\u00fcld\u00f6tt ellen\u0151rz\u0151 k\u00f3dot." + } }, "reauth_confirm": { "data": { @@ -22,6 +29,12 @@ "password": "Jelsz\u00f3" } }, + "reauth_mfa": { + "data": { + "code": "Ellen\u0151rz\u0151 k\u00f3d", + "description": "A fi\u00f3kj\u00e1ban be van \u00e1ll\u00edtva a 2 l\u00e9pcs\u0151s ellen\u0151rz\u00e9s. K\u00e9rem, adja meg a Verisure \u00e1ltal k\u00fcld\u00f6tt ellen\u0151rz\u0151 k\u00f3dot." + } + }, "user": { "data": { "description": "Jelentkezzen be a Verisure My Pages fi\u00f3kj\u00e1val.", diff --git a/homeassistant/components/verisure/translations/id.json b/homeassistant/components/verisure/translations/id.json index 5c9badda341..7f8dbe66817 100644 --- a/homeassistant/components/verisure/translations/id.json +++ b/homeassistant/components/verisure/translations/id.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autentikasi tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" + "unknown": "Kesalahan yang tidak diharapkan", + "unknown_mfa": "Terjadi kesalahan yang tidak diketahui dalam pengaturan MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant menemukan beberapa instalasi Verisure di akun My Pages. Pilih instalasi untuk ditambahkan ke Home Assistant." }, + "mfa": { + "data": { + "code": "Kode Verifikasi", + "description": "Verifikasi 2 langka tetap diaktifkan pada akun Anda. Masukkan kode verifikasi yang dikirimkan Verisure kepada Anda." + } + }, "reauth_confirm": { "data": { "description": "Autentikasi ulang dengan akun Verisure My Pages Anda.", @@ -22,6 +29,12 @@ "password": "Kata Sandi" } }, + "reauth_mfa": { + "data": { + "code": "Kode Verifikasi", + "description": "Verifikasi 2 langka tetap diaktifkan pada akun Anda. Masukkan kode verifikasi yang dikirimkan Verisure kepada Anda." + } + }, "user": { "data": { "description": "Masuk dengan akun Verisure My Pages Anda.", diff --git a/homeassistant/components/verisure/translations/it.json b/homeassistant/components/verisure/translations/it.json index e30976a37a6..8361765c5c2 100644 --- a/homeassistant/components/verisure/translations/it.json +++ b/homeassistant/components/verisure/translations/it.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autenticazione non valida", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "unknown_mfa": "Si \u00e8 verificato un errore sconosciuto durante la configurazione dell'autenticazione a pi\u00f9 fattori" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant ha trovato pi\u00f9 installazioni Verisure nel tuo account My Pages. Per favore, seleziona l'installazione da aggiungere a Home Assistant." }, + "mfa": { + "data": { + "code": "Codice di verifica", + "description": "Il tuo account ha la verifica in due passaggi abilitata. Inserisci il codice di verifica che ti viene inviato da Verisure." + } + }, "reauth_confirm": { "data": { "description": "Autenticati nuovamente con il tuo account Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Password" } }, + "reauth_mfa": { + "data": { + "code": "Codice di verifica", + "description": "Il tuo account ha la verifica in due passaggi abilitata. Inserisci il codice di verifica che ti viene inviato da Verisure." + } + }, "user": { "data": { "description": "Accedi con il tuo account Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/ja.json b/homeassistant/components/verisure/translations/ja.json index 5bd85b67e34..9aa412a0214 100644 --- a/homeassistant/components/verisure/translations/ja.json +++ b/homeassistant/components/verisure/translations/ja.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unknown_mfa": "MFA\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant\u306f\u3001\u30de\u30a4\u30da\u30fc\u30b8\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u8907\u6570\u306eVerisure\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u691c\u51fa\u3057\u307e\u3057\u305f\u3002Home Assistant\u306b\u8ffd\u52a0\u3059\u308b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "mfa": { + "data": { + "code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9", + "description": "\u30a2\u30ab\u30a6\u30f3\u30c8\u30672\u6bb5\u968e\u8a8d\u8a3c\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u3059\u3002Verisure\u304b\u3089\u9001\u4fe1\u3055\u308c\u305f\u78ba\u8a8d\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + }, "reauth_confirm": { "data": { "description": "Verisure MyPages\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002", @@ -22,6 +29,12 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } }, + "reauth_mfa": { + "data": { + "code": "\u78ba\u8a8d\u30b3\u30fc\u30c9", + "description": "\u30a2\u30ab\u30a6\u30f3\u30c8\u30672\u6bb5\u968e\u8a8d\u8a3c\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u3059\u3002Verisure\u304b\u3089\u9001\u4fe1\u3055\u308c\u305f\u78ba\u8a8d\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + }, "user": { "data": { "description": "Verisure My Pages\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/verisure/translations/nl.json b/homeassistant/components/verisure/translations/nl.json index 1a23da57319..89dc4860e95 100644 --- a/homeassistant/components/verisure/translations/nl.json +++ b/homeassistant/components/verisure/translations/nl.json @@ -15,6 +15,11 @@ }, "description": "Home Assistant heeft meerdere Verisure-installaties gevonden in uw My Pages-account. Selecteer de installatie om toe te voegen aan Home Assistant." }, + "mfa": { + "data": { + "code": "Verificatiecode" + } + }, "reauth_confirm": { "data": { "description": "Verifieer opnieuw met uw Verisure My Pages-account.", @@ -22,6 +27,11 @@ "password": "Wachtwoord" } }, + "reauth_mfa": { + "data": { + "code": "Verificatiecode" + } + }, "user": { "data": { "description": "Aanmelden met Verisure My Pages-account.", diff --git a/homeassistant/components/verisure/translations/pl.json b/homeassistant/components/verisure/translations/pl.json index baaf61f60c3..dcdf566f9d4 100644 --- a/homeassistant/components/verisure/translations/pl.json +++ b/homeassistant/components/verisure/translations/pl.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unknown_mfa": "Podczas konfigurowania us\u0142ugi MFA wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant znalaz\u0142 wiele instalacji Verisure na Twoim koncie. Wybierz instalacj\u0119, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistanta." }, + "mfa": { + "data": { + "code": "Kod weryfikacyjny", + "description": "Twoje konto ma w\u0142\u0105czon\u0105 weryfikacj\u0119 dwuetapow\u0105. Wprowad\u017a kod weryfikacyjny wys\u0142any przez Verisure." + } + }, "reauth_confirm": { "data": { "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Verisure.", @@ -22,6 +29,12 @@ "password": "Has\u0142o" } }, + "reauth_mfa": { + "data": { + "code": "Kod weryfikacyjny", + "description": "Twoje konto ma w\u0142\u0105czon\u0105 weryfikacj\u0119 dwuetapow\u0105. Wprowad\u017a kod weryfikacyjny wys\u0142any przez Verisure." + } + }, "user": { "data": { "description": "Zaloguj si\u0119 na swoje konto Verisure.", diff --git a/homeassistant/components/verisure/translations/pt-BR.json b/homeassistant/components/verisure/translations/pt-BR.json index 1fc733bfd55..9d42afd0e30 100644 --- a/homeassistant/components/verisure/translations/pt-BR.json +++ b/homeassistant/components/verisure/translations/pt-BR.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unknown_mfa": "Ocorreu um erro desconhecido durante a configura\u00e7\u00e3o da MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "O Home Assistant encontrou v\u00e1rias instala\u00e7\u00f5es da Verisure na sua conta do My Pages. Por favor, selecione a instala\u00e7\u00e3o para adicionar ao Home Assistant." }, + "mfa": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o", + "description": "Sua conta tem a verifica\u00e7\u00e3o em duas etapas ativada. Insira o c\u00f3digo de verifica\u00e7\u00e3o que a Verisure envia para voc\u00ea." + } + }, "reauth_confirm": { "data": { "description": "Re-autentique com sua conta Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Senha" } }, + "reauth_mfa": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o", + "description": "Sua conta tem a verifica\u00e7\u00e3o em duas etapas ativada. Insira o c\u00f3digo de verifica\u00e7\u00e3o que a Verisure envia para voc\u00ea." + } + }, "user": { "data": { "description": "Fa\u00e7a login com sua conta Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/ru.json b/homeassistant/components/verisure/translations/ru.json index 430b8d773d0..c3380b48f9f 100644 --- a/homeassistant/components/verisure/translations/ru.json +++ b/homeassistant/components/verisure/translations/ru.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unknown_mfa": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 MFA \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043e\u043a Verisure \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041c\u043e\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b\u00bb \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 Home Assistant." }, + "mfa": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f", + "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0434\u0432\u0443\u0445\u044d\u0442\u0430\u043f\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 Verisure \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442 \u0412\u0430\u043c." + } + }, "reauth_confirm": { "data": { "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.", @@ -22,6 +29,12 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c" } }, + "reauth_mfa": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f", + "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0434\u0432\u0443\u0445\u044d\u0442\u0430\u043f\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 Verisure \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442 \u0412\u0430\u043c." + } + }, "user": { "data": { "description": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.", diff --git a/homeassistant/components/verisure/translations/tr.json b/homeassistant/components/verisure/translations/tr.json index 88a0d943872..e339b3d6997 100644 --- a/homeassistant/components/verisure/translations/tr.json +++ b/homeassistant/components/verisure/translations/tr.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "unknown_mfa": "MFA kurulumu s\u0131ras\u0131nda bilinmeyen bir hata olu\u015ftu" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant, Sayfalar\u0131m hesab\u0131n\u0131zda birden fazla Verisure y\u00fcklemesi buldu. L\u00fctfen Home Assistant'a eklemek i\u00e7in kurulumu se\u00e7in." }, + "mfa": { + "data": { + "code": "Do\u011frulama kodu", + "description": "Hesab\u0131n\u0131zda 2 ad\u0131ml\u0131 do\u011frulama etkinle\u015ftirildi. L\u00fctfen Verisure'un size g\u00f6nderdi\u011fi do\u011frulama kodunu girin." + } + }, "reauth_confirm": { "data": { "description": "Verisure My Pages hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n.", @@ -22,6 +29,12 @@ "password": "Parola" } }, + "reauth_mfa": { + "data": { + "code": "Do\u011frulama kodu", + "description": "Hesab\u0131n\u0131zda 2 ad\u0131ml\u0131 do\u011frulama etkinle\u015ftirildi. L\u00fctfen Verisure'un size g\u00f6nderdi\u011fi do\u011frulama kodunu girin." + } + }, "user": { "data": { "description": "Verisure My Pages hesab\u0131n\u0131zla oturum a\u00e7\u0131n.", diff --git a/homeassistant/components/verisure/translations/zh-Hant.json b/homeassistant/components/verisure/translations/zh-Hant.json index 29410e390fe..53eb4ab1eb7 100644 --- a/homeassistant/components/verisure/translations/zh-Hant.json +++ b/homeassistant/components/verisure/translations/zh-Hant.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unknown_mfa": "\u65bc\u591a\u6b65\u9a5f\u8a8d\u8b49\u8a2d\u5b9a\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant \u65bc My Pages \u5e33\u865f\u4e2d\u627e\u5230\u591a\u500b Verisure \u5b89\u88dd\u3002\u8acb\u9078\u64c7\u6240\u8981\u65b0\u589e\u81f3 Home Assistant \u7684\u9805\u76ee\u3002" }, + "mfa": { + "data": { + "code": "\u9a57\u8b49\u78bc", + "description": "\u5e33\u865f\u5177\u6709\u5169\u6b65\u9a5f\u9a57\u8b49\u3001\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684 Verisure \u9a57\u8b49\u78bc\u3002" + } + }, "reauth_confirm": { "data": { "description": "\u91cd\u65b0\u8a8d\u8b49 Verisure My Pages \u5e33\u865f\u3002", @@ -22,6 +29,12 @@ "password": "\u5bc6\u78bc" } }, + "reauth_mfa": { + "data": { + "code": "\u9a57\u8b49\u78bc", + "description": "\u5e33\u865f\u5177\u6709\u5169\u6b65\u9a5f\u9a57\u8b49\u3001\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684 Verisure \u9a57\u8b49\u78bc\u3002" + } + }, "user": { "data": { "description": "\u4ee5 Verisure My Pages \u5e33\u865f\u767b\u5165", diff --git a/homeassistant/components/version/__init__.py b/homeassistant/components/version/__init__.py index 22dbad36d7b..878ed3d0138 100644 --- a/homeassistant/components/version/__init__.py +++ b/homeassistant/components/version/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/version/translations/cs.json b/homeassistant/components/version/translations/cs.json index 33006d6761b..16fbcb2c8d4 100644 --- a/homeassistant/components/version/translations/cs.json +++ b/homeassistant/components/version/translations/cs.json @@ -2,6 +2,11 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "step": { + "user": { + "title": "Vyberte typ instalace" + } } } } \ No newline at end of file diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index eb18a1229ca..10aa49514e5 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -54,22 +54,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b fans = hass.data[DOMAIN][VS_FANS] = [] lights = hass.data[DOMAIN][VS_LIGHTS] = [] sensors = hass.data[DOMAIN][VS_SENSORS] = [] + platforms = [] if device_dict[VS_SWITCHES]: switches.extend(device_dict[VS_SWITCHES]) - hass.async_create_task(forward_setup(config_entry, Platform.SWITCH)) + platforms.append(Platform.SWITCH) if device_dict[VS_FANS]: fans.extend(device_dict[VS_FANS]) - hass.async_create_task(forward_setup(config_entry, Platform.FAN)) + platforms.append(Platform.FAN) if device_dict[VS_LIGHTS]: lights.extend(device_dict[VS_LIGHTS]) - hass.async_create_task(forward_setup(config_entry, Platform.LIGHT)) + platforms.append(Platform.LIGHT) if device_dict[VS_SENSORS]: sensors.extend(device_dict[VS_SENSORS]) - hass.async_create_task(forward_setup(config_entry, Platform.SENSOR)) + platforms.append(Platform.SENSOR) + + await hass.config_entries.async_forward_entry_setups(config_entry, platforms) async def async_new_device_discovery(service: ServiceCall) -> None: """Discover if new devices should be added.""" diff --git a/homeassistant/components/vesync/translations/ja.json b/homeassistant/components/vesync/translations/ja.json index 66d7cf26ca9..c6717072946 100644 --- a/homeassistant/components/vesync/translations/ja.json +++ b/homeassistant/components/vesync/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 221f3a4374a..20d237ee4e4 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(setup_vicare_api, hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index db0ce9cddaa..575ca35729b 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,7 +3,7 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.16.4"], + "requirements": ["PyViCare==2.16.2"], "iot_class": "cloud_polling", "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/vicare/translations/ja.json b/homeassistant/components/vicare/translations/ja.json index aa7e159778c..8d50ef180d1 100644 --- a/homeassistant/components/vicare/translations/ja.json +++ b/homeassistant/components/vicare/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { @@ -16,7 +16,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" }, - "description": "ViCare\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.viessmann.com \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "ViCare\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.viessmann.com \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/vicare/translations/pt.json b/homeassistant/components/vicare/translations/pt.json new file mode 100644 index 00000000000..6a34ef9530f --- /dev/null +++ b/homeassistant/components/vicare/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "client_id": "Chave da API", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py index ac3b8b87f3f..82fe4c7bb70 100644 --- a/homeassistant/components/vilfo/__init__.py +++ b/homeassistant/components/vilfo/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = vilfo_router - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 1e0d5a322fb..38cf916a5e6 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_refresh() hass.data[DOMAIN][CONF_APPS] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vlc_telnet/__init__.py b/homeassistant/components/vlc_telnet/__init__.py index 9fe3b97ab31..a8a0c16be8e 100644 --- a/homeassistant/components/vlc_telnet/__init__.py +++ b/homeassistant/components/vlc_telnet/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: domain_data = hass.data.setdefault(DOMAIN, {}) domain_data[entry.entry_id] = {DATA_VLC: vlc, DATA_AVAILABLE: available} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vlc_telnet/translations/hu.json b/homeassistant/components/vlc_telnet/translations/hu.json index bac42b4d4c3..b1247a74d2e 100644 --- a/homeassistant/components/vlc_telnet/translations/hu.json +++ b/homeassistant/components/vlc_telnet/translations/hu.json @@ -21,7 +21,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg a helyes jelsz\u00f3t: {host}" + "description": "K\u00e9rem, adja meg a helyes jelsz\u00f3t: {host}" }, "user": { "data": { diff --git a/homeassistant/components/vlc_telnet/translations/pt.json b/homeassistant/components/vlc_telnet/translations/pt.json new file mode 100644 index 00000000000..55ccd56b497 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py index 8189aeef1f4..77119b9a65e 100644 --- a/homeassistant/components/volumio/__init__.py +++ b/homeassistant/components/volumio/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_INFO: info, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vulcan/__init__.py b/homeassistant/components/vulcan/__init__.py index 50f525d9a68..8e66075935b 100644 --- a/homeassistant/components/vulcan/__init__.py +++ b/homeassistant/components/vulcan/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from err hass.data[DOMAIN][entry.entry_id] = client - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vulcan/translations/hu.json b/homeassistant/components/vulcan/translations/hu.json index 65d90d6e24a..953eb90146f 100644 --- a/homeassistant/components/vulcan/translations/hu.json +++ b/homeassistant/components/vulcan/translations/hu.json @@ -3,13 +3,13 @@ "abort": { "all_student_already_configured": "M\u00e1r minden tanul\u00f3 hozz\u00e1adva.", "already_configured": "Ezt a tanul\u00f3t m\u00e1r hozz\u00e1adt\u00e1k.", - "no_matching_entries": "Nem tal\u00e1ltunk megfelel\u0151 bejegyz\u00e9st, k\u00e9rj\u00fck, haszn\u00e1ljon m\u00e1sik fi\u00f3kot, vagy t\u00e1vol\u00edtsa el az elavult di\u00e1kkal val\u00f3 integr\u00e1ci\u00f3t...", + "no_matching_entries": "Nem tal\u00e1lhat\u00f3 megfelel\u0151 bejegyz\u00e9s, k\u00e9rem, haszn\u00e1ljon m\u00e1sik fi\u00f3kot, vagy t\u00e1vol\u00edtsa el az elavult di\u00e1kkal val\u00f3 integr\u00e1ci\u00f3t...", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres" }, "error": { "cannot_connect": "Csatlakoz\u00e1si hiba \u2013 ellen\u0151rizze az internetkapcsolatot", - "expired_credentials": "Lej\u00e1rt hiteles\u00edt\u0151 adatok - k\u00e9rj\u00fck, hozzon l\u00e9tre \u00fajakat a Vulcan mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n.", - "expired_token": "Lej\u00e1rt token \u2013 k\u00e9rj\u00fck, hozzon l\u00e9tre egy \u00fajat", + "expired_credentials": "Lej\u00e1rt hiteles\u00edt\u0151 adatok - k\u00e9rem, hozzon l\u00e9tre \u00fajakat a Vulcan mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n.", + "expired_token": "Lej\u00e1rt token \u2013 k\u00e9rem, hozzon l\u00e9tre egy \u00fajat", "invalid_pin": "\u00c9rv\u00e9nytelen PIN-k\u00f3d", "invalid_symbol": "\u00c9rv\u00e9nytelen szimb\u00f3lum", "invalid_token": "\u00c9rv\u00e9nytelen token", diff --git a/homeassistant/components/vulcan/translations/ja.json b/homeassistant/components/vulcan/translations/ja.json index 4f18d47b7ea..5d6f5bbcca7 100644 --- a/homeassistant/components/vulcan/translations/ja.json +++ b/homeassistant/components/vulcan/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "all_student_already_configured": "\u3059\u3079\u3066\u306e\u751f\u5f92\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_configured": "\u305d\u306e\u5b66\u751f\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", - "no_matching_entries": "\u4e00\u81f4\u3059\u308b\u30a8\u30f3\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u304b\u3001outdated student\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044..", + "no_matching_entries": "\u4e00\u81f4\u3059\u308b\u30a8\u30f3\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u304b\u3001outdated student\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044..", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f" }, "error": { @@ -48,7 +48,7 @@ "data": { "student_name": "\u751f\u5f92\u3092\u9078\u629e(Select student)" }, - "description": "\u751f\u5f92\u3092\u9078\u629e\u3057\u307e\u3059(Select student)\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3067\u3001\u3088\u308a\u591a\u304f\u306e\u751f\u5f92\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" + "description": "\u751f\u5f92\u3092\u9078\u629e\u3057\u307e\u3059(Select student)\u3002\u7d71\u5408\u3092\u518d\u5ea6\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3067\u3001\u3088\u308a\u591a\u304f\u306e\u751f\u5f92\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index e959f4b33f3..fc2c36eed80 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -2,7 +2,7 @@ "domain": "wake_on_lan", "name": "Wake on LAN", "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", - "requirements": ["wakeonlan==2.0.1"], + "requirements": ["wakeonlan==2.1.0"], "codeowners": ["@ntilley905"], "iot_class": "local_push" } diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index ae003d84a9c..f0392f808ae 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -208,7 +208,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = wallbox_coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 0e4e1477911..9d1db637879 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -3,7 +3,10 @@ from homeassistant.backports.enum import StrEnum DOMAIN = "wallbox" +BIDIRECTIONAL_MODEL_PREFIXES = ["QSX"] + CONF_STATION = "station" +CHARGER_ADDED_DISCHARGED_ENERGY_KEY = "added_discharged_energy" CHARGER_ADDED_ENERGY_KEY = "added_energy" CHARGER_ADDED_RANGE_KEY = "added_range" CHARGER_CHARGING_POWER_KEY = "charging_power" diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 1db791fd389..5470ec11532 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -1,4 +1,4 @@ -"""Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" +"""Home Assistant component for accessing the Wallbox Portal API. The number component allows control of charging current.""" from __future__ import annotations from dataclasses import dataclass @@ -11,9 +11,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import InvalidAuth, WallboxCoordinator, WallboxEntity from .const import ( + BIDIRECTIONAL_MODEL_PREFIXES, CHARGER_DATA_KEY, CHARGER_MAX_AVAILABLE_POWER_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_PART_NUMBER_KEY, CHARGER_SERIAL_NUMBER_KEY, DOMAIN, ) @@ -21,14 +23,13 @@ from .const import ( @dataclass class WallboxNumberEntityDescription(NumberEntityDescription): - """Describes Wallbox sensor entity.""" + """Describes Wallbox number entity.""" NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { CHARGER_MAX_CHARGING_CURRENT_KEY: WallboxNumberEntityDescription( key=CHARGER_MAX_CHARGING_CURRENT_KEY, name="Max. Charging Current", - native_min_value=6, ), } @@ -36,7 +37,7 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Create wallbox sensor entities in HASS.""" + """Create wallbox number entities in HASS.""" coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] # Check if the user is authorized to change current, if so, add number component: try: @@ -66,21 +67,30 @@ class WallboxNumber(WallboxEntity, NumberEntity): entry: ConfigEntry, description: WallboxNumberEntityDescription, ) -> None: - """Initialize a Wallbox sensor.""" + """Initialize a Wallbox number entity.""" super().__init__(coordinator) self.entity_description = description self._coordinator = coordinator self._attr_name = f"{entry.title} {description.name}" self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}" + self._is_bidirectional = ( + coordinator.data[CHARGER_DATA_KEY][CHARGER_PART_NUMBER_KEY][0:3] + in BIDIRECTIONAL_MODEL_PREFIXES + ) @property def native_max_value(self) -> float: """Return the maximum available current.""" return cast(float, self._coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY]) + @property + def native_min_value(self) -> float: + """Return the minimum available current based on charger type - some chargers can discharge.""" + return (self.max_value * -1) if self._is_bidirectional else 6 + @property def native_value(self) -> float | None: - """Return the state of the sensor.""" + """Return the value of the entity.""" return cast( Optional[float], self._coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY] ) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index e3598ca7e07..2c4a8c67bed 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -25,6 +25,7 @@ from homeassistant.helpers.typing import StateType from . import WallboxCoordinator, WallboxEntity from .const import ( + CHARGER_ADDED_DISCHARGED_ENERGY_KEY, CHARGER_ADDED_ENERGY_KEY, CHARGER_ADDED_RANGE_KEY, CHARGER_CHARGING_POWER_KEY, @@ -94,6 +95,14 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + CHARGER_ADDED_DISCHARGED_ENERGY_KEY: WallboxSensorEntityDescription( + key=CHARGER_ADDED_DISCHARGED_ENERGY_KEY, + name="Discharged Energy", + precision=2, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), CHARGER_COST_KEY: WallboxSensorEntityDescription( key=CHARGER_COST_KEY, icon="mdi:ev-station", diff --git a/homeassistant/components/wallbox/translations/pt.json b/homeassistant/components/wallbox/translations/pt.json new file mode 100644 index 00000000000..ece60e5e010 --- /dev/null +++ b/homeassistant/components/wallbox/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index 2f07658b923..cac73f597f6 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 41842c6ed70..040753d97e8 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -35,14 +35,14 @@ SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT = "percent" REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_MOER, - name="Marginal Operating Emissions Rate", + name="Marginal operating emissions rate", icon="mdi:blur", native_unit_of_measurement=f"{MASS_POUNDS} CO2/MWh", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT, - name="Relative Marginal Emissions Intensity", + name="Relative marginal emissions intensity", icon="mdi:blur", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -67,6 +67,8 @@ async def async_setup_entry( class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): """Define a realtime emissions sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, diff --git a/homeassistant/components/watttime/translations/ja.json b/homeassistant/components/watttime/translations/ja.json index 87b71e0675f..93d36406019 100644 --- a/homeassistant/components/watttime/translations/ja.json +++ b/homeassistant/components/watttime/translations/ja.json @@ -28,7 +28,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/watttime/translations/pt.json b/homeassistant/components/watttime/translations/pt.json new file mode 100644 index 00000000000..859d8de1627 --- /dev/null +++ b/homeassistant/components/watttime/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 3b193d6c06b..4e82af5119c 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index 545c0cdb0d6..3463699509a 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -31,7 +31,7 @@ "units": "\u5358\u4f4d", "vehicle_type": "\u8eca\u4e21\u30bf\u30a4\u30d7" }, - "description": "`substring`\u30a4\u30f3\u30d7\u30c3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3088\u3046\u306b\u5f37\u5236\u3057\u305f\u308a\u3001\u9006\u306b\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u56de\u907f\u3057\u305f\u30bf\u30a4\u30e0\u30c8\u30e9\u30d9\u30eb\u306e\u8a08\u7b97\u3092\u884c\u3046\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" + "description": "`substring`\u30a4\u30f3\u30d7\u30c3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u7d71\u5408\u3067\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3088\u3046\u306b\u5f37\u5236\u3057\u305f\u308a\u3001\u9006\u306b\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u56de\u907f\u3057\u305f\u30bf\u30a4\u30e0\u30c8\u30e9\u30d9\u30eb\u306e\u8a08\u7b97\u3092\u884c\u3046\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/waze_travel_time/translations/pt.json b/homeassistant/components/waze_travel_time/translations/pt.json new file mode 100644 index 00000000000..3d986f4533d --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 1fdb9173646..c55ae043622 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -171,16 +171,16 @@ class Forecast(TypedDict, total=False): datetime: str precipitation_probability: int | None native_precipitation: float | None - precipitation: float | None + precipitation: None native_pressure: float | None - pressure: float | None + pressure: None native_temperature: float | None - temperature: float | None + temperature: None native_templow: float | None - templow: float | None + templow: None wind_bearing: float | str | None native_wind_speed: float | None - wind_speed: float | None + wind_speed: None async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -218,33 +218,33 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_precision: float - _attr_pressure: float | None = ( + _attr_pressure: None = ( None # Provide backwards compatibility. Use _attr_native_pressure ) - _attr_pressure_unit: str | None = ( + _attr_pressure_unit: None = ( None # Provide backwards compatibility. Use _attr_native_pressure_unit ) _attr_state: None = None - _attr_temperature: float | None = ( + _attr_temperature: None = ( None # Provide backwards compatibility. Use _attr_native_temperature ) - _attr_temperature_unit: str | None = ( + _attr_temperature_unit: None = ( None # Provide backwards compatibility. Use _attr_native_temperature_unit ) - _attr_visibility: float | None = ( + _attr_visibility: None = ( None # Provide backwards compatibility. Use _attr_native_visibility ) - _attr_visibility_unit: str | None = ( + _attr_visibility_unit: None = ( None # Provide backwards compatibility. Use _attr_native_visibility_unit ) - _attr_precipitation_unit: str | None = ( + _attr_precipitation_unit: None = ( None # Provide backwards compatibility. Use _attr_native_precipitation_unit ) _attr_wind_bearing: float | str | None = None - _attr_wind_speed: float | None = ( + _attr_wind_speed: None = ( None # Provide backwards compatibility. Use _attr_native_wind_speed ) - _attr_wind_speed_unit: str | None = ( + _attr_wind_speed_unit: None = ( None # Provide backwards compatibility. Use _attr_native_wind_speed_unit ) @@ -299,7 +299,7 @@ class WeatherEntity(Entity): and module.__file__ and "custom_components" in module.__file__ ): - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " @@ -321,6 +321,7 @@ class WeatherEntity(Entity): return self.async_registry_entry_updated() + @final @property def temperature(self) -> float | None: """Return the temperature for backward compatibility. @@ -345,6 +346,7 @@ class WeatherEntity(Entity): return self._attr_native_temperature_unit + @final @property def temperature_unit(self) -> str | None: """Return the temperature unit for backward compatibility. @@ -376,6 +378,7 @@ class WeatherEntity(Entity): return self._default_temperature_unit + @final @property def pressure(self) -> float | None: """Return the pressure for backward compatibility. @@ -400,6 +403,7 @@ class WeatherEntity(Entity): return self._attr_native_pressure_unit + @final @property def pressure_unit(self) -> str | None: """Return the pressure unit for backward compatibility. @@ -436,6 +440,7 @@ class WeatherEntity(Entity): """Return the humidity in native units.""" return self._attr_humidity + @final @property def wind_speed(self) -> float | None: """Return the wind_speed for backward compatibility. @@ -460,6 +465,7 @@ class WeatherEntity(Entity): return self._attr_native_wind_speed_unit + @final @property def wind_speed_unit(self) -> str | None: """Return the wind_speed unit for backward compatibility. @@ -505,6 +511,7 @@ class WeatherEntity(Entity): """Return the ozone level.""" return self._attr_ozone + @final @property def visibility(self) -> float | None: """Return the visibility for backward compatibility. @@ -529,6 +536,7 @@ class WeatherEntity(Entity): return self._attr_native_visibility_unit + @final @property def visibility_unit(self) -> str | None: """Return the visibility unit for backward compatibility. @@ -573,6 +581,7 @@ class WeatherEntity(Entity): return self._attr_native_precipitation_unit + @final @property def precipitation_unit(self) -> str | None: """Return the precipitation unit for backward compatibility. diff --git a/homeassistant/components/weather/translations/pt.json b/homeassistant/components/weather/translations/pt.json index 5875b8a7192..13c5273632c 100644 --- a/homeassistant/components/weather/translations/pt.json +++ b/homeassistant/components/weather/translations/pt.json @@ -1,13 +1,13 @@ { "state": { "_": { - "clear-night": "Limpo, Noite", + "clear-night": "C\u00e9u limpo, Noite", "cloudy": "Nublado", "exceptional": "Excecional", "fog": "Nevoeiro", "hail": "Granizo", "lightning": "Rel\u00e2mpago", - "lightning-rainy": "Rel\u00e2mpagos, chuva", + "lightning-rainy": "Trovoada, chuva", "partlycloudy": "Parcialmente nublado", "pouring": "Chuva forte", "rainy": "Chuva", diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index fb9927f1b37..449de006bf9 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -6,7 +6,9 @@ from http import HTTPStatus from ipaddress import ip_address import logging import secrets +from typing import TYPE_CHECKING, Any +from aiohttp import StreamReader from aiohttp.web import Request, Response import voluptuous as vol @@ -17,7 +19,7 @@ from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import network -from homeassistant.util.aiohttp import MockRequest, serialize_response +from homeassistant.util.aiohttp import MockRequest, MockStreamReader, serialize_response _LOGGER = logging.getLogger(__name__) @@ -83,17 +85,20 @@ def async_generate_path(webhook_id: str) -> str: @bind_hass async def async_handle_webhook( - hass: HomeAssistant, webhook_id: str, request: Request + hass: HomeAssistant, webhook_id: str, request: Request | MockRequest ) -> Response: """Handle a webhook.""" - handlers = hass.data.setdefault(DOMAIN, {}) + handlers: dict[str, dict[str, Any]] = hass.data.setdefault(DOMAIN, {}) # Always respond successfully to not give away if a hook exists or not. if (webhook := handlers.get(webhook_id)) is None: + content_stream: StreamReader | MockStreamReader if isinstance(request, MockRequest): received_from = request.mock_source + content_stream = request.content else: received_from = request.remote + content_stream = request.content _LOGGER.info( "Received message for unregistered webhook %s from %s", @@ -102,13 +107,16 @@ async def async_handle_webhook( ) # Look at content to provide some context for received webhook # Limit to 64 chars to avoid flooding the log - content = await request.content.read(64) + content = await content_stream.read(64) _LOGGER.debug("%s", content) return Response(status=HTTPStatus.OK) if webhook["local_only"]: + if TYPE_CHECKING: + assert isinstance(request, Request) + assert request.remote is not None try: - remote = ip_address(request.remote) # type: ignore[arg-type] + remote = ip_address(request.remote) except ValueError: _LOGGER.debug("Unable to parse remote ip %s", request.remote) return Response(status=HTTPStatus.OK) diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 3f790b1ec42..498a7363a61 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -1,5 +1,7 @@ """Offer webhook triggered automation rules.""" -from functools import partial +from __future__ import annotations + +from dataclasses import dataclass from aiohttp import hdrs import voluptuous as vol @@ -13,7 +15,7 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import async_register, async_unregister +from . import DOMAIN, async_register, async_unregister # mypy: allow-untyped-defs @@ -26,20 +28,35 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( } ) +WEBHOOK_TRIGGERS = f"{DOMAIN}_triggers" -async def _handle_webhook(job, trigger_data, hass, webhook_id, request): + +@dataclass +class TriggerInstance: + """Attached trigger settings.""" + + automation_info: AutomationTriggerInfo + job: HassJob + + +async def _handle_webhook(hass, webhook_id, request): """Handle incoming webhook.""" - result = {"platform": "webhook", "webhook_id": webhook_id} + base_result = {"platform": "webhook", "webhook_id": webhook_id} if "json" in request.headers.get(hdrs.CONTENT_TYPE, ""): - result["json"] = await request.json() + base_result["json"] = await request.json() else: - result["data"] = await request.post() + base_result["data"] = await request.post() - result["query"] = request.query - result["description"] = "webhook" - result.update(**trigger_data) - hass.async_run_hass_job(job, {"trigger": result}) + base_result["query"] = request.query + base_result["description"] = "webhook" + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} + ) + for trigger in triggers[webhook_id]: + result = {**base_result, **trigger.automation_info["trigger_data"]} + hass.async_run_hass_job(trigger.job, {"trigger": result}) async def async_attach_trigger( @@ -49,20 +66,32 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" - trigger_data = automation_info["trigger_data"] webhook_id: str = config[CONF_WEBHOOK_ID] job = HassJob(action) - async_register( - hass, - automation_info["domain"], - automation_info["name"], - webhook_id, - partial(_handle_webhook, job, trigger_data), + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} ) + if webhook_id not in triggers: + async_register( + hass, + automation_info["domain"], + automation_info["name"], + webhook_id, + _handle_webhook, + ) + triggers[webhook_id] = [] + + trigger_instance = TriggerInstance(automation_info, job) + triggers[webhook_id].append(trigger_instance) + @callback def unregister(): """Unregister webhook.""" - async_unregister(hass, webhook_id) + triggers[webhook_id].remove(trigger_instance) + if not triggers[webhook_id]: + async_unregister(hass, webhook_id) + triggers.pop(webhook_id) return unregister diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index e759077ad3e..d9b2acb1836 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = wrapper - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. diff --git a/homeassistant/components/webostv/translations/hu.json b/homeassistant/components/webostv/translations/hu.json index de3f59d8c90..b4ef5f39aa1 100644 --- a/homeassistant/components/webostv/translations/hu.json +++ b/homeassistant/components/webostv/translations/hu.json @@ -6,7 +6,7 @@ "error_pairing": "Csatlakozva az LG webOS TV-hez, de a p\u00e1ros\u00edt\u00e1s nem siker\u00fclt" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, kapcsolja be a TV-t vagy ellen\u0151rizze az ip-c\u00edmet." + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rem, kapcsolja be a TV-t vagy ellen\u0151rizze az ip-c\u00edmet." }, "flow_title": "LG webOS Smart TV", "step": { diff --git a/homeassistant/components/webostv/translations/pt.json b/homeassistant/components/webostv/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/webostv/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index bea08722eb0..6c18fd96627 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,7 +1,6 @@ """Commands part of Websocket API.""" from __future__ import annotations -import asyncio from collections.abc import Callable import datetime as dt import json @@ -22,6 +21,7 @@ from homeassistant.exceptions import ( TemplateError, Unauthorized, ) +from homeassistant.generated import supported_brands from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( @@ -31,7 +31,12 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.json import JSON_DUMP, ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.loader import ( + Integration, + IntegrationNotFound, + async_get_integration, + async_get_integrations, +) from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations from homeassistant.util.json import ( find_paths_unserializable_data, @@ -68,6 +73,7 @@ def async_register_commands( async_reg(hass, handle_unsubscribe_events) async_reg(hass, handle_validate_config) async_reg(hass, handle_subscribe_entities) + async_reg(hass, handle_supported_brands) def pong_message(iden: int) -> dict[str, Any]: @@ -370,9 +376,13 @@ async def handle_manifest_list( wanted_integrations = msg.get("integrations") if wanted_integrations is None: wanted_integrations = async_get_loaded_integrations(hass) - integrations = await asyncio.gather( - *(async_get_integration(hass, domain) for domain in wanted_integrations) - ) + + ints_or_excs = await async_get_integrations(hass, wanted_integrations) + integrations: list[Integration] = [] + for int_or_exc in ints_or_excs.values(): + if isinstance(int_or_exc, Exception): + raise int_or_exc + integrations.append(int_or_exc) connection.send_result( msg["id"], [integration.manifest for integration in integrations] ) @@ -691,3 +701,25 @@ async def handle_validate_config( result[key] = {"valid": True, "error": None} connection.send_result(msg["id"], result) + + +@decorators.websocket_command( + { + vol.Required("type"): "supported_brands", + } +) +@decorators.async_response +async def handle_supported_brands( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle supported brands command.""" + data = {} + + ints_or_excs = await async_get_integrations( + hass, supported_brands.HAS_SUPPORTED_BRANDS + ) + for int_or_exc in ints_or_excs.values(): + if isinstance(int_or_exc, Exception): + raise int_or_exc + data[int_or_exc.domain] = int_or_exc.manifest["supported_brands"] + connection.send_result(msg["id"], data) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 26c4c6f8321..87c52288bcc 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.auth.models import RefreshToken, User from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized -from homeassistant.helpers.json import JSON_DUMP from . import const, messages @@ -54,13 +53,6 @@ class ActiveConnection: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) - async def send_big_result(self, msg_id: int, result: Any) -> None: - """Send a result message that would be expensive to JSON serialize.""" - content = await self.hass.async_add_executor_job( - JSON_DUMP, messages.result_message(msg_id, result) - ) - self.send_message(content) - @callback def send_error(self, msg_id: int, code: str, message: str) -> None: """Send a error message.""" diff --git a/homeassistant/components/wemo/translations/ja.json b/homeassistant/components/wemo/translations/ja.json index f86e1e80520..7ab4a0bfc1b 100644 --- a/homeassistant/components/wemo/translations/ja.json +++ b/homeassistant/components/wemo/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 659b4602f0d..1991ec3806c 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {AUTH_INSTANCE_KEY: auth} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/whirlpool/translations/pt.json b/homeassistant/components/whirlpool/translations/pt.json new file mode 100644 index 00000000000..ce1bf4bb4b8 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whois/__init__.py b/homeassistant/components/whois/__init__.py index aa64ebecf83..746e83a2677 100644 --- a/homeassistant/components/whois/__init__.py +++ b/homeassistant/components/whois/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 48efaf7630d..68e77162a4f 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -79,7 +79,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), WhoisSensorEntityDescription( key="days_until_expiration", - name="Days Until Expiration", + name="Days until expiration", icon="mdi:calendar-clock", native_unit_of_measurement=TIME_DAYS, value_fn=_days_until_expiration, @@ -93,7 +93,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), WhoisSensorEntityDescription( key="last_updated", - name="Last Updated", + name="Last updated", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda domain: _ensure_timezone(domain.last_updated), @@ -156,6 +156,7 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity): """Implementation of a WHOIS sensor.""" entity_description: WhoisSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -166,10 +167,10 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity): """Initialize the sensor.""" super().__init__(coordinator=coordinator) self.entity_description = description - self._attr_name = f"{domain} {description.name}" self._attr_unique_id = f"{domain}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, domain)}, + name=domain, entry_type=DeviceEntryType.SERVICE, ) self._domain = domain diff --git a/homeassistant/components/whois/translations/ja.json b/homeassistant/components/whois/translations/ja.json index 66a7868a265..132ebf9e070 100644 --- a/homeassistant/components/whois/translations/ja.json +++ b/homeassistant/components/whois/translations/ja.json @@ -6,7 +6,7 @@ "error": { "unexpected_response": "Whois\u30b5\u30fc\u30d0\u30fc\u304b\u3089\u306e\u4e88\u671f\u3057\u306a\u3044\u5fdc\u7b54", "unknown_date_format": "Whois\u30b5\u30fc\u30d0\u30fc\u306e\u5fdc\u7b54\u3067\u4e0d\u660e\u306a\u65e5\u4ed8\u30d5\u30a9\u30fc\u30de\u30c3\u30c8", - "unknown_tld": "\u6307\u5b9a\u3055\u308c\u305fTLD\u306f\u4e0d\u660e\u3001\u3082\u3057\u304f\u306f\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093", + "unknown_tld": "\u6307\u5b9a\u3055\u308c\u305fTLD\u306f\u4e0d\u660e\u3001\u3082\u3057\u304f\u306f\u3053\u306e\u7d71\u5408\u3067\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093", "whois_command_failed": "Whois\u30b3\u30de\u30f3\u30c9\u304c\u5931\u6557\u3057\u307e\u3057\u305f: whois\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "step": { diff --git a/homeassistant/components/whois/translations/pt.json b/homeassistant/components/whois/translations/pt.json new file mode 100644 index 00000000000..d252c078a2c --- /dev/null +++ b/homeassistant/components/whois/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index f2d11d53862..0472a0bc3b3 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Port %s already in use", entry.data[CONF_PORT]) raise ConfigEntryNotReady from exc - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 2cdcf20c1ea..fefde1644ad 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = parent # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 6e8dee9a774..da2174c3822 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -6,6 +6,7 @@ For more details about this platform, please refer to the documentation at from __future__ import annotations import asyncio +from typing import Any from aiohttp.web import Request, Response import voluptuous as vol @@ -103,7 +104,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Withings from a config entry.""" - config_updates = {} + config_updates: dict[str, Any] = {} # Add a unique id if it's an older config entry. if entry.unique_id != entry.data["token"]["userid"] or not isinstance( @@ -153,7 +154,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Start subscription check in the background, outside this component's setup. async_call_later(hass, 1, async_call_later_callback) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -197,7 +198,7 @@ async def async_webhook_handler( return json_message_response("Parameter appli not provided", message_code=20) try: - appli = NotifyAppli(int(params.getone("appli"))) + appli = NotifyAppli(int(params.getone("appli"))) # type: ignore[arg-type] except ValueError: return json_message_response("Invalid appli provided", message_code=21) diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index b0af5051124..ff98e6e0d45 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -32,6 +32,6 @@ class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.OCCUPANCY @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self._state_data diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index ad40378eafb..93c3800e42f 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -10,7 +10,7 @@ from enum import Enum, IntEnum from http import HTTPStatus import logging import re -from typing import Any +from typing import Any, Union from aiohttp.web import Response import requests @@ -32,7 +32,7 @@ from homeassistant.components.application_credentials import AuthImplementation from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_WEBHOOK_ID, MASS_KILOGRAMS, @@ -57,6 +57,7 @@ from . import const from .const import Measurement _LOGGER = logging.getLogger(const.LOG_NAMESPACE) +_RETRY_COEFFICIENT = 0.5 NOT_AUTHENTICATED_ERROR = re.compile( f"^{HTTPStatus.UNAUTHORIZED},.*", re.IGNORECASE, @@ -484,7 +485,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): ) -> None: """Initialize object.""" self._hass = hass - self._config_entry = config_entry + self.config_entry = config_entry self._implementation = implementation self.session = OAuth2Session(hass, config_entry, implementation) @@ -496,7 +497,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): self.session.async_ensure_token_valid(), self._hass.loop ).result() - access_token = self._config_entry.data["token"]["access_token"] + access_token = self.config_entry.data["token"]["access_token"] response = requests.request( method, f"{self.URL}/{path}", @@ -588,7 +589,7 @@ class DataManager: update_method=self.async_subscribe_webhook, ) self.poll_data_update_coordinator = DataUpdateCoordinator[ - dict[MeasureType, Any] + Union[dict[MeasureType, Any], None] ]( hass, _LOGGER, @@ -651,7 +652,7 @@ class DataManager: "Failed attempt %s of %s (%s)", attempt, attempts, exception1 ) # Make each backoff pause a little bit longer - await asyncio.sleep(0.5 * attempt) + await asyncio.sleep(_RETRY_COEFFICIENT * attempt) exception = exception1 continue @@ -738,32 +739,8 @@ class DataManager: if isinstance( exception, (UnauthorizedException, AuthFailedException) ) or NOT_AUTHENTICATED_ERROR.match(str(exception)): - context = { - const.PROFILE: self._profile, - "userid": self._user_id, - "source": SOURCE_REAUTH, - } - - # Check if reauth flow already exists. - flow = next( - iter( - flow - for flow in self._hass.config_entries.flow.async_progress_by_handler( - const.DOMAIN - ) - if flow.context == context - ), - None, - ) - if flow: - return - - # Start a reauth flow. - await self._hass.config_entries.flow.async_init( - const.DOMAIN, - context=context, - ) - return + self._api.config_entry.async_start_reauth(self._hass) + return None raise exception @@ -974,7 +951,7 @@ class BaseWithingsSensor(Entity): return self._unique_id @property - def icon(self) -> str: + def icon(self) -> str | None: """Icon to use in the frontend, if any.""" return self._attribute.icon @@ -1028,7 +1005,7 @@ async def async_get_data_manager( config_entry_data = hass.data[const.DOMAIN][config_entry.entry_id] if const.DATA_MANAGER not in config_entry_data: - profile = config_entry.data.get(const.PROFILE) + profile: str = config_entry.data[const.PROFILE] _LOGGER.debug("Creating withings data manager for profile: %s", profile) config_entry_data[const.DATA_MANAGER] = DataManager( diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index a4ac6597248..b0fa1876d92 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -8,7 +8,6 @@ from typing import Any import voluptuous as vol from withings_api.common import AuthScope -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util import slugify @@ -25,6 +24,7 @@ class WithingsFlowHandler( # Temporarily holds authorization data during the profile step. _current_data: dict[str, None | str | int] = {} + _reauth_profile: str | None = None @property def logger(self) -> logging.Logger: @@ -53,12 +53,7 @@ class WithingsFlowHandler( async def async_step_profile(self, data: dict[str, Any]) -> FlowResult: """Prompt the user to select a user profile.""" errors = {} - reauth_profile = ( - self.context.get(const.PROFILE) - if self.context.get("source") == SOURCE_REAUTH - else None - ) - profile = data.get(const.PROFILE) or reauth_profile + profile = data.get(const.PROFILE) or self._reauth_profile if profile: existing_entries = [ @@ -67,7 +62,7 @@ class WithingsFlowHandler( if slugify(config_entry.data.get(const.PROFILE)) == slugify(profile) ] - if reauth_profile or not existing_entries: + if self._reauth_profile or not existing_entries: new_data = {**self._current_data, **data, const.PROFILE: profile} self._current_data = {} return await self.async_step_finish(new_data) @@ -81,16 +76,23 @@ class WithingsFlowHandler( ) async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Prompt user to re-authenticate.""" + self._reauth_profile = data.get(const.PROFILE) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, data: dict[str, Any] | None = None + ) -> FlowResult: """Prompt user to re-authenticate.""" if data is not None: return await self.async_step_user() - placeholders = {const.PROFILE: self.context["profile"]} + placeholders = {const.PROFILE: self._reauth_profile} self.context.update({"title_placeholders": placeholders}) return self.async_show_form( - step_id="reauth", + step_id="reauth_confirm", description_placeholders=placeholders, ) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 433888d9ebf..8f8a32c95e7 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -10,7 +10,7 @@ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, - "reauth": { + "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data." } diff --git a/homeassistant/components/withings/translations/af.json b/homeassistant/components/withings/translations/af.json new file mode 100644 index 00000000000..3a1c3f97dbf --- /dev/null +++ b/homeassistant/components/withings/translations/af.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Herverifieer integrasie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ar.json b/homeassistant/components/withings/translations/ar.json new file mode 100644 index 00000000000..8db98ed45e5 --- /dev/null +++ b/homeassistant/components/withings/translations/ar.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u0625\u0639\u0627\u062f\u0629 \u0645\u0635\u0627\u062f\u0642\u0629 \u0627\u0644\u062a\u0643\u0627\u0645\u0644" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/bg.json b/homeassistant/components/withings/translations/bg.json index 9d5313ee391..ad1284c7a48 100644 --- a/homeassistant/components/withings/translations/bg.json +++ b/homeassistant/components/withings/translations/bg.json @@ -20,6 +20,9 @@ }, "reauth": { "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } } diff --git a/homeassistant/components/withings/translations/bn.json b/homeassistant/components/withings/translations/bn.json new file mode 100644 index 00000000000..97f6140ff26 --- /dev/null +++ b/homeassistant/components/withings/translations/bn.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u0987\u09a8\u09cd\u099f\u09bf\u0997\u09cd\u09b0\u09c7\u09b6\u09a8 \u09aa\u09c1\u09a8\u09b0\u09be\u09af\u09bc \u09aa\u09cd\u09b0\u09ae\u09be\u09a3\u09c0\u0995\u09b0\u09a3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/bs.json b/homeassistant/components/withings/translations/bs.json new file mode 100644 index 00000000000..14583e8ae80 --- /dev/null +++ b/homeassistant/components/withings/translations/bs.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Ponovo potvrdite integraciju" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index c75e08e0234..91be6ffabbe 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -27,6 +27,10 @@ "reauth": { "description": "El perfil \"{profile}\" s'ha de tornar a autenticar per poder continuar rebent dades de Withings.", "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "reauth_confirm": { + "description": "El perfil \"{profile}\" s'ha de tornar a autenticar per poder continuar rebent dades de Withings.", + "title": "Reautenticar la integraci\u00f3" } } } diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index 31d5ad2f6e5..672ced9ca5c 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -27,6 +27,10 @@ "reauth": { "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.", "title": "Integration erneut authentifizieren" + }, + "reauth_confirm": { + "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.", + "title": "Integration erneut authentifizieren" } } } diff --git a/homeassistant/components/withings/translations/el.json b/homeassistant/components/withings/translations/el.json index dc284985ee2..068d347467a 100644 --- a/homeassistant/components/withings/translations/el.json +++ b/homeassistant/components/withings/translations/el.json @@ -27,6 +27,10 @@ "reauth": { "description": "\u03a4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \"{profile}\" \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 Withings.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "reauth_confirm": { + "description": "\u03a4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \"{profile}\" \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 Withings.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } } diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json index e8acc8c3440..490e60512f9 100644 --- a/homeassistant/components/withings/translations/en.json +++ b/homeassistant/components/withings/translations/en.json @@ -27,6 +27,10 @@ "reauth": { "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", "title": "Reauthenticate Integration" + }, + "reauth_confirm": { + "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", + "title": "Reauthenticate Integration" } } } diff --git a/homeassistant/components/withings/translations/et.json b/homeassistant/components/withings/translations/et.json index 5395069522f..a336b6b0bdf 100644 --- a/homeassistant/components/withings/translations/et.json +++ b/homeassistant/components/withings/translations/et.json @@ -27,6 +27,10 @@ "reauth": { "description": "Withingi andmete jsaamiseks tuleb kasutaja {profile} taastuvastada.", "title": "Taastuvasta sidumine" + }, + "reauth_confirm": { + "description": "Profiil \"{profile}\" tuleb uuesti tuvastada, et j\u00e4tkata Withingsi andmete saamist.", + "title": "Taastuvasta sidumine" } } } diff --git a/homeassistant/components/withings/translations/eu.json b/homeassistant/components/withings/translations/eu.json new file mode 100644 index 00000000000..fa2d5af97c2 --- /dev/null +++ b/homeassistant/components/withings/translations/eu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Berriro autentifikatu Integrazioa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index f59b8e2e714..e0d9e8db08d 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -25,7 +25,11 @@ "title": "Profil utilisateur" }, "reauth": { - "description": "Le profile \" {profile} \" doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", + "description": "Le profile \u00ab\u00a0{profile}\u00a0\u00bb doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "reauth_confirm": { + "description": "Le profile \u00ab\u00a0{profile}\u00a0\u00bb doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", "title": "R\u00e9-authentifier l'int\u00e9gration" } } diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index 7504c36c58e..a157d5e3688 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -27,6 +27,10 @@ "reauth": { "description": "A \u201e{profile}\u201d profilt \u00fajra hiteles\u00edteni kell, hogy tov\u00e1bbra is fogadni tudja a Withings adatokat.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "reauth_confirm": { + "description": "A \u201e{profile}\u201d profilt \u00fajra kell hiteles\u00edteni, hogy tov\u00e1bbra is megkaphassa a Withings-adatokat.", + "title": "Az integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } } diff --git a/homeassistant/components/withings/translations/hy.json b/homeassistant/components/withings/translations/hy.json new file mode 100644 index 00000000000..92c4ce24080 --- /dev/null +++ b/homeassistant/components/withings/translations/hy.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u054e\u0565\u0580\u0561\u0570\u0561\u057d\u057f\u0561\u057f\u0565\u056c \u056b\u0576\u057f\u0565\u0563\u0580\u0578\u0582\u0574\u0568" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/id.json b/homeassistant/components/withings/translations/id.json index eb21a0d3352..4b4490a617b 100644 --- a/homeassistant/components/withings/translations/id.json +++ b/homeassistant/components/withings/translations/id.json @@ -27,6 +27,10 @@ "reauth": { "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.", "title": "Autentikasi Ulang Integrasi" + }, + "reauth_confirm": { + "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.", + "title": "Autentikasi Ulang Integrasi" } } } diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 079acb0b503..30836be5da5 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -27,6 +27,10 @@ "reauth": { "description": "Il profilo \"{profile}\" deve essere autenticato nuovamente per continuare a ricevere i dati Withings.", "title": "Autentica nuovamente l'integrazione" + }, + "reauth_confirm": { + "description": "Il profilo \"{profile}\" deve essere nuovamente autenticato per continuare a ricevere i dati di Withings.", + "title": "Autentica nuovamente l'integrazione" } } } diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index bc70bf0c746..3fdcffdb918 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -26,7 +26,11 @@ }, "reauth": { "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, + "reauth_confirm": { + "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index cca755effbb..8e40180b75e 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -27,6 +27,9 @@ "reauth": { "description": "Het {profile} \" moet opnieuw worden geverifieerd om Withings-gegevens te blijven ontvangen.", "title": "Integratie herauthenticeren" + }, + "reauth_confirm": { + "title": "Integratie herauthenticeren" } } } diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index ff8d18eec64..40ba24111bf 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -27,6 +27,9 @@ "reauth": { "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", "title": "Godkjenne integrering p\u00e5 nytt" + }, + "reauth_confirm": { + "title": "Re-autentiser integrasjon" } } } diff --git a/homeassistant/components/withings/translations/pl.json b/homeassistant/components/withings/translations/pl.json index 638171af846..27544b29fa4 100644 --- a/homeassistant/components/withings/translations/pl.json +++ b/homeassistant/components/withings/translations/pl.json @@ -27,6 +27,10 @@ "reauth": { "description": "Profil \"{profile}\" musi zosta\u0107 ponownie uwierzytelniony, aby nadal otrzymywa\u0107 dane Withings.", "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "reauth_confirm": { + "description": "Profil \"{profile}\" musi zosta\u0107 ponownie uwierzytelniony, aby nadal otrzymywa\u0107 dane Withings.", + "title": "Ponownie uwierzytelnij integracj\u0119" } } } diff --git a/homeassistant/components/withings/translations/pt-BR.json b/homeassistant/components/withings/translations/pt-BR.json index 6a067498f1e..4ea2fd4a92c 100644 --- a/homeassistant/components/withings/translations/pt-BR.json +++ b/homeassistant/components/withings/translations/pt-BR.json @@ -27,6 +27,10 @@ "reauth": { "description": "O perfil \"{profile}\" precisa ser autenticado novamente para continuar recebendo dados do Withings", "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "O perfil \"{profile}\" precisa ser autenticado novamente para continuar recebendo dados do Withings.", + "title": "Reautenticar Integra\u00e7\u00e3o" } } } diff --git a/homeassistant/components/withings/translations/pt.json b/homeassistant/components/withings/translations/pt.json index 1fe7083ecfd..fa97013c5c0 100644 --- a/homeassistant/components/withings/translations/pt.json +++ b/homeassistant/components/withings/translations/pt.json @@ -15,10 +15,14 @@ "profile": { "data": { "profile": "Perfil" - } + }, + "description": "Fornecer um nome de perfil \u00fanico para estes dados. Normalmente, este \u00e9 o nome do perfil que seleccionou na etapa anterior." }, "reauth": { "title": "Re-autenticar Perfil" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" } } } diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index 7127f9545fa..78f8f3ec5e4 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -27,6 +27,10 @@ "reauth": { "description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "reauth_confirm": { + "description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/withings/translations/tr.json b/homeassistant/components/withings/translations/tr.json index 31b2a424071..698fe41988c 100644 --- a/homeassistant/components/withings/translations/tr.json +++ b/homeassistant/components/withings/translations/tr.json @@ -27,6 +27,10 @@ "reauth": { "description": "Withings verilerini almaya devam etmek i\u00e7in \" {profile}", "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "reauth_confirm": { + "description": "Withings verilerini almaya devam etmek i\u00e7in \" {profile} \" profilinin yeniden do\u011frulanmas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" } } } diff --git a/homeassistant/components/withings/translations/zh-Hans.json b/homeassistant/components/withings/translations/zh-Hans.json new file mode 100644 index 00000000000..83a6258ba49 --- /dev/null +++ b/homeassistant/components/withings/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u91cd\u65b0\u9a8c\u8bc1\u96c6\u6210" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json index 2ee7ce3d3da..35328ea9353 100644 --- a/homeassistant/components/withings/translations/zh-Hant.json +++ b/homeassistant/components/withings/translations/zh-Hant.json @@ -27,6 +27,10 @@ "reauth": { "description": "\"{profile}\" \u8a2d\u5b9a\u6a94\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u4ee5\u4fdd\u6301\u63a5\u6536 Withings \u8cc7\u6599\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "reauth_confirm": { + "description": "\"{profile}\" \u8a2d\u5b9a\u6a94\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u4ee5\u4fdd\u6301\u63a5\u6536 Withings \u8cc7\u6599\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } } diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index b47db32d90f..396893b04b1 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -133,7 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = WizData( coordinator=coordinator, bulb=bulb, scenes=scenes ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/wiz/binary_sensor.py b/homeassistant/components/wiz/binary_sensor.py index 1ecb3125215..538bd3a741d 100644 --- a/homeassistant/components/wiz/binary_sensor.py +++ b/homeassistant/components/wiz/binary_sensor.py @@ -66,12 +66,12 @@ class WizOccupancyEntity(WizEntity, BinarySensorEntity): """Representation of WiZ Occupancy sensor.""" _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + _attr_name = "Occupancy" def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize an WiZ device.""" super().__init__(wiz_data, name) self._attr_unique_id = OCCUPANCY_UNIQUE_ID.format(self._device.mac) - self._attr_name = f"{name} Occupancy" self._async_update_attrs() @callback diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py index c78f3e3b37b..633fb71f165 100644 --- a/homeassistant/components/wiz/entity.py +++ b/homeassistant/components/wiz/entity.py @@ -21,13 +21,14 @@ from .models import WizData class WizEntity(CoordinatorEntity[DataUpdateCoordinator[Optional[float]]], Entity): """Representation of WiZ entity.""" + _attr_has_entity_name = True + def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize a WiZ entity.""" super().__init__(wiz_data.coordinator) self._device = wiz_data.bulb bulb_type: BulbType = self._device.bulbtype self._attr_unique_id = self._device.mac - self._attr_name = name self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, self._device.mac)}, name=name, diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index d2f68fcf7c3..9fd700f8f9c 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -52,7 +52,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_max_value=200, native_step=1, icon="mdi:speedometer", - name="Effect Speed", + name="Effect speed", value_fn=lambda device: cast(Optional[int], device.state.get_speed()), set_value_fn=_async_set_speed, required_feature="effect", @@ -63,7 +63,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_max_value=100, native_step=1, icon="mdi:floor-lamp-dual", - name="Dual Head Ratio", + name="Dual head ratio", value_fn=lambda device: cast(Optional[int], device.state.get_ratio()), set_value_fn=_async_set_ratio, required_feature="dual_head", @@ -98,7 +98,6 @@ class WizSpeedNumber(WizEntity, NumberEntity): super().__init__(wiz_data, name) self.entity_description = description self._attr_unique_id = f"{self._device.mac}_{description.key}" - self._attr_name = f"{name} {description.name}" self._async_update_attrs() @property diff --git a/homeassistant/components/wiz/sensor.py b/homeassistant/components/wiz/sensor.py index 11f3933fd16..d2042d6ea9c 100644 --- a/homeassistant/components/wiz/sensor.py +++ b/homeassistant/components/wiz/sensor.py @@ -20,7 +20,7 @@ from .models import WizData SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="rssi", - name="Signal Strength", + name="Signal strength", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -33,7 +33,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( POWER_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="power", - name="Current Power", + name="Current power", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, @@ -73,7 +73,6 @@ class WizSensor(WizEntity, SensorEntity): super().__init__(wiz_data, name) self.entity_description = description self._attr_unique_id = f"{self._device.mac}_{description.key}" - self._attr_name = f"{name} {description.name}" self._async_update_attrs() @callback diff --git a/homeassistant/components/wiz/translations/hu.json b/homeassistant/components/wiz/translations/hu.json index 0c27ee730b3..90c3b3b35a2 100644 --- a/homeassistant/components/wiz/translations/hu.json +++ b/homeassistant/components/wiz/translations/hu.json @@ -6,7 +6,7 @@ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { - "bulb_time_out": "Nem lehet csatlakoztatni az izz\u00f3hoz. Lehet, hogy az izz\u00f3 offline \u00e1llapotban van, vagy rossz IP-t adott meg. K\u00e9rj\u00fck, kapcsolja fel a l\u00e1mp\u00e1t, \u00e9s pr\u00f3b\u00e1lja \u00fajra!", + "bulb_time_out": "Nem lehet csatlakoztatni az izz\u00f3hoz. Lehet, hogy az izz\u00f3 offline \u00e1llapotban van, vagy rossz IP-t adott meg. K\u00e9rem, kapcsolja fel a l\u00e1mp\u00e1t, \u00e9s pr\u00f3b\u00e1lja \u00fajra!", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "no_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm.", "no_wiz_light": "Az izz\u00f3 nem csatlakoztathat\u00f3 a WiZ Platform integr\u00e1ci\u00f3n kereszt\u00fcl.", diff --git a/homeassistant/components/wiz/translations/ja.json b/homeassistant/components/wiz/translations/ja.json index e062fbdb75b..b8e1536654b 100644 --- a/homeassistant/components/wiz/translations/ja.json +++ b/homeassistant/components/wiz/translations/ja.json @@ -9,7 +9,7 @@ "bulb_time_out": "\u96fb\u7403\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u96fb\u7403\u304c\u30aa\u30d5\u30e9\u30a4\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u9593\u9055\u3063\u305fIP/\u30db\u30b9\u30c8\u304c\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u96fb\u7403\u306e\u96fb\u6e90\u3092\u5165\u308c\u3066\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_ip": "\u6709\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", - "no_wiz_light": "\u3053\u306e\u96fb\u7403\u306f\u3001WiZ Platform\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "no_wiz_light": "\u3053\u306e\u96fb\u7403\u306f\u3001WiZ Platform\u7d71\u5408\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/wiz/translations/pt.json b/homeassistant/components/wiz/translations/pt.json new file mode 100644 index 00000000000..b686fee56b5 --- /dev/null +++ b/homeassistant/components/wiz/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index bcfb98b9916..809afdfb3c7 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Reload entry when its updated. entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py index d2262798d50..61f8dc45f7b 100644 --- a/homeassistant/components/wled/binary_sensor.py +++ b/homeassistant/components/wled/binary_sensor.py @@ -34,6 +34,7 @@ class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.UPDATE + _attr_name = "Firmware" # Disabled by default, as this entity is deprecated. _attr_entity_registry_enabled_default = False @@ -41,7 +42,6 @@ class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Firmware" self._attr_unique_id = f"{coordinator.data.info.mac_address}_update" @property diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 97877053163..b08ee396c70 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -28,11 +28,11 @@ class WLEDRestartButton(WLEDEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Restart" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Restart" self._attr_unique_id = f"{coordinator.data.info.mac_address}_restart" @wled_exception_handler diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 4f5c758dfff..98be359628e 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -52,13 +52,13 @@ class WLEDMasterLight(WLEDEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_icon = "mdi:led-strip-variant" + _attr_name = "Master" _attr_supported_features = LightEntityFeature.TRANSITION _attr_supported_color_modes = {ColorMode.BRIGHTNESS} def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED master light.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Master" self._attr_unique_id = coordinator.data.info.mac_address @property @@ -118,9 +118,8 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - self._attr_name = f"{coordinator.data.info.name} Segment {segment}" - if segment == 0: - self._attr_name = coordinator.data.info.name + if segment != 0: + self._attr_name = f"Segment {segment}" self._attr_unique_id = ( f"{self.coordinator.data.info.mac_address}_{self._segment}" diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 668950b9326..2fc00131fac 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.13.2"], + "requirements": ["wled==0.14.1"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py index b5fc0855e04..2bdd2e46e2c 100644 --- a/homeassistant/components/wled/models.py +++ b/homeassistant/components/wled/models.py @@ -10,6 +10,8 @@ from .coordinator import WLEDDataUpdateCoordinator class WLEDEntity(CoordinatorEntity[WLEDDataUpdateCoordinator]): """Defines a base WLED entity.""" + _attr_has_entity_name = True + @property def device_info(self) -> DeviceInfo: """Return device information about this WLED device.""" diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index 6c426cc44c5..d6032791e5b 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -71,11 +71,8 @@ class WLEDNumber(WLEDEntity, NumberEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - self._attr_name = ( - f"{coordinator.data.info.name} Segment {segment} {description.name}" - ) - if segment == 0: - self._attr_name = f"{coordinator.data.info.name} {description.name}" + if segment != 0: + self._attr_name = f"Segment {segment} {description.name}" self._attr_unique_id = ( f"{coordinator.data.info.mac_address}_{description.key}_{segment}" diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index e555b3422ce..c3980f9f9c7 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -51,12 +51,12 @@ class WLEDLiveOverrideSelect(WLEDEntity, SelectEntity): _attr_device_class = DEVICE_CLASS_WLED_LIVE_OVERRIDE _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:theater" + _attr_name = "Live override" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED .""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Live Override" self._attr_unique_id = f"{coordinator.data.info.mac_address}_live_override" self._attr_options = [str(live.value) for live in Live] @@ -75,12 +75,12 @@ class WLEDPresetSelect(WLEDEntity, SelectEntity): """Defined a WLED Preset select.""" _attr_icon = "mdi:playlist-play" + _attr_name = "Preset" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED .""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Preset" self._attr_unique_id = f"{coordinator.data.info.mac_address}_preset" self._attr_options = [preset.name for preset in self.coordinator.data.presets] @@ -106,12 +106,12 @@ class WLEDPlaylistSelect(WLEDEntity, SelectEntity): """Define a WLED Playlist select.""" _attr_icon = "mdi:play-speed" + _attr_name = "Playlist" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED playlist.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Playlist" self._attr_unique_id = f"{coordinator.data.info.mac_address}_playlist" self._attr_options = [ playlist.name for playlist in self.coordinator.data.playlists @@ -140,6 +140,7 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity): _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:palette-outline" + _attr_name = "Color palette" _segment: int def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: @@ -148,11 +149,8 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - self._attr_name = ( - f"{coordinator.data.info.name} Segment {segment} Color Palette" - ) - if segment == 0: - self._attr_name = f"{coordinator.data.info.name} Color Palette" + if segment != 0: + self._attr_name = f"Segment {segment} color palette" self._attr_unique_id = f"{coordinator.data.info.mac_address}_palette_{segment}" self._attr_options = [ diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 720158938c7..4a677910273 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -50,7 +50,7 @@ class WLEDSensorEntityDescription( SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( WLEDSensorEntityDescription( key="estimated_current", - name="Estimated Current", + name="Estimated current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -60,13 +60,13 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="info_leds_count", - name="LED Count", + name="LED count", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.leds.count, ), WLEDSensorEntityDescription( key="info_leds_max_power", - name="Max Current", + name="Max current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.CURRENT, @@ -83,7 +83,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="free_heap", - name="Free Memory", + name="Free memory", icon="mdi:memory", native_unit_of_measurement=DATA_BYTES, state_class=SensorStateClass.MEASUREMENT, @@ -93,7 +93,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="wifi_signal", - name="Wi-Fi Signal", + name="Wi-Fi signal", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -111,7 +111,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="wifi_channel", - name="Wi-Fi Channel", + name="Wi-Fi channel", icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -155,7 +155,6 @@ class WLEDSensorEntity(WLEDEntity, SensorEntity): """Initialize a WLED sensor entity.""" super().__init__(coordinator=coordinator) self.entity_description = description - self._attr_name = f"{coordinator.data.info.name} {description.name}" self._attr_unique_id = f"{coordinator.data.info.mac_address}_{description.key}" @property diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index e98b3494ad6..7d0d9ee24fb 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -55,11 +55,11 @@ class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:weather-night" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Nightlight" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED nightlight switch.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Nightlight" self._attr_unique_id = f"{coordinator.data.info.mac_address}_nightlight" @property @@ -92,11 +92,11 @@ class WLEDSyncSendSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:upload-network-outline" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Sync send" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync send switch.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Sync Send" self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_send" @property @@ -125,11 +125,11 @@ class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:download-network-outline" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Sync receive" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync receive switch.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Sync Receive" self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_receive" @property @@ -158,6 +158,7 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:swap-horizontal-bold" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Reverse" _segment: int def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: @@ -166,9 +167,8 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - self._attr_name = f"{coordinator.data.info.name} Segment {segment} Reverse" - if segment == 0: - self._attr_name = f"{coordinator.data.info.name} Reverse" + if segment != 0: + self._attr_name = f"Segment {segment} reverse" self._attr_unique_id = f"{coordinator.data.info.mac_address}_reverse_{segment}" self._segment = segment diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index 5f30617d7eb..af3654ea2b0 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "cct_unsupported": "\u3053\u306eWLED\u30c7\u30d0\u30a4\u30b9\u306fCCT\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "cct_unsupported": "\u3053\u306eWLED\u30c7\u30d0\u30a4\u30b9\u306fCCT\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/wled/translations/pt.json b/homeassistant/components/wled/translations/pt.json index 313c9057da0..cc9f0af829a 100644 --- a/homeassistant/components/wled/translations/pt.json +++ b/homeassistant/components/wled/translations/pt.json @@ -12,6 +12,9 @@ "data": { "host": "Nome servidor ou endere\u00e7o IP" } + }, + "zeroconf_confirm": { + "title": "Dispositivo WLED descoberto" } } } diff --git a/homeassistant/components/wled/translations/select.pt.json b/homeassistant/components/wled/translations/select.pt.json new file mode 100644 index 00000000000..d33123843d4 --- /dev/null +++ b/homeassistant/components/wled/translations/select.pt.json @@ -0,0 +1,8 @@ +{ + "state": { + "wled__live_override": { + "0": "Desligado", + "1": "Ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/update.py b/homeassistant/components/wled/update.py index f0fc532b3b3..75546fdac1a 100644 --- a/homeassistant/components/wled/update.py +++ b/homeassistant/components/wled/update.py @@ -36,11 +36,11 @@ class WLEDUpdateEntity(WLEDEntity, UpdateEntity): UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION ) _attr_title = "WLED" + _attr_name = "Firmware" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the update entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Firmware" self._attr_unique_id = coordinator.data.info.mac_address @property diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 19cafa89a13..9d76c61806b 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -106,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][COORDINATOR] = coordinator hass.data[DOMAIN][entry.entry_id][DEVICE_ID] = device_id - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wolflink/translations/sensor.bg.json b/homeassistant/components/wolflink/translations/sensor.bg.json index 8d0335dcc31..6d41058320d 100644 --- a/homeassistant/components/wolflink/translations/sensor.bg.json +++ b/homeassistant/components/wolflink/translations/sensor.bg.json @@ -5,6 +5,7 @@ "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", "dhw_prior": "DHWPrior", "gasdruck": "\u041d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 \u0433\u0430\u0437\u0430", + "heizung": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "kalibration": "\u041a\u0430\u043b\u0438\u0431\u0440\u0438\u0440\u0430\u043d\u0435", "test": "\u0422\u0435\u0441\u0442", "tpw": "TPW", diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index dea1b470b9e..0b40ce84816 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -104,7 +104,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ws66i/translations/pt.json b/homeassistant/components/ws66i/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/ws66i/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py deleted file mode 100644 index 6a7aba16b95..00000000000 --- a/homeassistant/components/xbee/__init__.py +++ /dev/null @@ -1,441 +0,0 @@ -"""Support for XBee Zigbee devices.""" -# pylint: disable=import-error -from binascii import hexlify, unhexlify -import logging - -from serial import Serial, SerialException -import voluptuous as vol -from xbee_helper import ZigBee -import xbee_helper.const as xb_const -from xbee_helper.device import convert_adc -from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure - -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - CONF_ADDRESS, - CONF_DEVICE, - CONF_NAME, - CONF_PIN, - EVENT_HOMEASSISTANT_STOP, - PERCENTAGE, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -SIGNAL_XBEE_FRAME_RECEIVED = "xbee_frame_received" - -CONF_BAUD = "baud" - -DEFAULT_DEVICE = "/dev/ttyUSB0" -DEFAULT_BAUD = 9600 -DEFAULT_ADC_MAX_VOLTS = 1.2 - -ATTR_FRAME = "frame" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string, - vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -PLATFORM_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_PIN): cv.positive_int, - vol.Optional(CONF_ADDRESS): cv.string, - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the connection to the XBee Zigbee device.""" - usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE) - baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD)) - try: - ser = Serial(usb_device, baud) - except SerialException as exc: - _LOGGER.exception("Unable to open serial port for XBee: %s", exc) - return False - zigbee_device = ZigBee(ser) - - def close_serial_port(*args): - """Close the serial port we're using to communicate with the XBee.""" - zigbee_device.zb.serial.close() - - def _frame_received(frame): - """Run when a XBee Zigbee frame is received. - - Pickles the frame, then encodes it into base64 since it contains - non JSON serializable binary. - """ - dispatcher_send(hass, SIGNAL_XBEE_FRAME_RECEIVED, frame) - - hass.data[DOMAIN] = zigbee_device - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_serial_port) - zigbee_device.add_frame_rx_handler(_frame_received) - - return True - - -def frame_is_relevant(entity, frame): - """Test whether the frame is relevant to the entity.""" - if frame.get("source_addr_long") != entity.config.address: - return False - return "samples" in frame - - -class XBeeConfig: - """Handle the fetching of configuration from the config file.""" - - def __init__(self, config): - """Initialize the configuration.""" - self._config = config - self._should_poll = config.get("poll", True) - - @property - def name(self): - """Return the name given to the entity.""" - return self._config["name"] - - @property - def address(self): - """Return the address of the device. - - If an address has been provided, unhexlify it, otherwise return None - as we're talking to our local XBee device. - """ - if (address := self._config.get("address")) is not None: - return unhexlify(address) - return address - - @property - def should_poll(self): - """Return the polling state.""" - return self._should_poll - - -class XBeePinConfig(XBeeConfig): - """Handle the fetching of configuration from the configuration file.""" - - @property - def pin(self): - """Return the GPIO pin number.""" - return self._config["pin"] - - -class XBeeDigitalInConfig(XBeePinConfig): - """A subclass of XBeePinConfig.""" - - def __init__(self, config): - """Initialise the XBee Zigbee Digital input config.""" - super().__init__(config) - self._bool2state, self._state2bool = self.boolean_maps - - @property - def boolean_maps(self): - """Create mapping dictionaries for potential inversion of booleans. - - Create dicts to map the pin state (true/false) to potentially inverted - values depending on the on_state config value which should be set to - "low" or "high". - """ - if self._config.get("on_state", "").lower() == "low": - bool2state = {True: False, False: True} - else: - bool2state = {True: True, False: False} - state2bool = {v: k for k, v in bool2state.items()} - return bool2state, state2bool - - @property - def bool2state(self): - """Return a dictionary mapping the internal value to the Zigbee value. - - For the translation of on/off as being pin high or low. - """ - return self._bool2state - - @property - def state2bool(self): - """Return a dictionary mapping the Zigbee value to the internal value. - - For the translation of pin high/low as being on or off. - """ - return self._state2bool - - -class XBeeDigitalOutConfig(XBeePinConfig): - """A subclass of XBeePinConfig. - - Set _should_poll to default as False instead of True. The value will - still be overridden by the presence of a 'poll' config entry. - """ - - def __init__(self, config): - """Initialize the XBee Zigbee Digital out.""" - super().__init__(config) - self._bool2state, self._state2bool = self.boolean_maps - self._should_poll = config.get("poll", False) - - @property - def boolean_maps(self): - """Create dicts to map booleans to pin high/low and vice versa. - - Depends on the config item "on_state" which should be set to "low" - or "high". - """ - if self._config.get("on_state", "").lower() == "low": - bool2state = { - True: xb_const.GPIO_DIGITAL_OUTPUT_LOW, - False: xb_const.GPIO_DIGITAL_OUTPUT_HIGH, - } - else: - bool2state = { - True: xb_const.GPIO_DIGITAL_OUTPUT_HIGH, - False: xb_const.GPIO_DIGITAL_OUTPUT_LOW, - } - state2bool = {v: k for k, v in bool2state.items()} - return bool2state, state2bool - - @property - def bool2state(self): - """Return a dictionary mapping booleans to GPIOSetting objects. - - For the translation of on/off as being pin high or low. - """ - return self._bool2state - - @property - def state2bool(self): - """Return a dictionary mapping GPIOSetting objects to booleans. - - For the translation of pin high/low as being on or off. - """ - return self._state2bool - - -class XBeeAnalogInConfig(XBeePinConfig): - """Representation of a XBee Zigbee GPIO pin set to analog in.""" - - @property - def max_voltage(self): - """Return the voltage for ADC to report its highest value.""" - return float(self._config.get("max_volts", DEFAULT_ADC_MAX_VOLTS)) - - -class XBeeDigitalIn(Entity): - """Representation of a GPIO pin configured as a digital input.""" - - def __init__(self, config, device): - """Initialize the device.""" - self._config = config - self._device = device - self._state = False - - async def async_added_to_hass(self): - """Register callbacks.""" - - def handle_frame(frame): - """Handle an incoming frame. - - Handle an incoming frame and update our status if it contains - information relating to this device. - """ - if not frame_is_relevant(self, frame): - return - sample = next(iter(frame["samples"])) - pin_name = xb_const.DIGITAL_PINS[self._config.pin] - if pin_name not in sample: - # Doesn't contain information about our pin - return - # Set state to the value of sample, respecting any inversion - # logic from the on_state config variable. - self._state = self._config.state2bool[ - self._config.bool2state[sample[pin_name]] - ] - self.schedule_update_ha_state() - - async_dispatcher_connect(self.hass, SIGNAL_XBEE_FRAME_RECEIVED, handle_frame) - - @property - def name(self): - """Return the name of the input.""" - return self._config.name - - @property - def config(self): - """Return the entity's configuration.""" - return self._config - - @property - def should_poll(self): - """Return the state of the polling, if needed.""" - return self._config.should_poll - - @property - def is_on(self): - """Return True if the Entity is on, else False.""" - return self._state - - def update(self): - """Ask the Zigbee device what state its input pin is in.""" - try: - sample = self._device.get_sample(self._config.address) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get sample from " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - return - except ZigBeeException as exc: - _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) - return - pin_name = xb_const.DIGITAL_PINS[self._config.pin] - if pin_name not in sample: - _LOGGER.warning( - "Pin %s (%s) was not in the sample provided by Zigbee device %s", - self._config.pin, - pin_name, - hexlify(self._config.address), - ) - return - self._state = self._config.state2bool[sample[pin_name]] - - -class XBeeDigitalOut(XBeeDigitalIn): - """Representation of a GPIO pin configured as a digital input.""" - - def _set_state(self, state): - """Initialize the XBee Zigbee digital out device.""" - try: - self._device.set_gpio_pin( - self._config.pin, self._config.bool2state[state], self._config.address - ) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to set output pin on " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - return - except ZigBeeException as exc: - _LOGGER.exception("Unable to set digital pin on XBee device: %s", exc) - return - self._state = state - if not self.should_poll: - self.schedule_update_ha_state() - - def turn_on(self, **kwargs): - """Set the digital output to its 'on' state.""" - self._set_state(True) - - def turn_off(self, **kwargs): - """Set the digital output to its 'off' state.""" - self._set_state(False) - - def update(self): - """Ask the XBee device what its output is set to.""" - try: - pin_state = self._device.get_gpio_pin( - self._config.pin, self._config.address - ) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get output pin status" - " from Zigbee device at address: %s", - hexlify(self._config.address), - ) - return - except ZigBeeException as exc: - _LOGGER.exception( - "Unable to get output pin status from XBee device: %s", exc - ) - return - self._state = self._config.state2bool[pin_state] - - -class XBeeAnalogIn(SensorEntity): - """Representation of a GPIO pin configured as an analog input.""" - - _attr_native_unit_of_measurement = PERCENTAGE - - def __init__(self, config, device): - """Initialize the XBee analog in device.""" - self._config = config - self._device = device - self._value = None - - async def async_added_to_hass(self): - """Register callbacks.""" - - def handle_frame(frame): - """Handle an incoming frame. - - Handle an incoming frame and update our status if it contains - information relating to this device. - """ - if not frame_is_relevant(self, frame): - return - sample = frame["samples"].pop() - pin_name = xb_const.ANALOG_PINS[self._config.pin] - if pin_name not in sample: - # Doesn't contain information about our pin - return - self._value = convert_adc( - sample[pin_name], xb_const.ADC_PERCENTAGE, self._config.max_voltage - ) - self.schedule_update_ha_state() - - async_dispatcher_connect(self.hass, SIGNAL_XBEE_FRAME_RECEIVED, handle_frame) - - @property - def name(self): - """Return the name of the input.""" - return self._config.name - - @property - def config(self): - """Return the entity's configuration.""" - return self._config - - @property - def should_poll(self): - """Return the polling state, if needed.""" - return self._config.should_poll - - @property - def sensor_state(self): - """Return the state of the entity.""" - return self._value - - def update(self): - """Get the latest reading from the ADC.""" - try: - self._value = self._device.read_analog_pin( - self._config.pin, - self._config.max_voltage, - self._config.address, - xb_const.ADC_PERCENTAGE, - ) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get sample from " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - except ZigBeeException as exc: - _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) diff --git a/homeassistant/components/xbee/binary_sensor.py b/homeassistant/components/xbee/binary_sensor.py deleted file mode 100644 index b1639085993..00000000000 --- a/homeassistant/components/xbee/binary_sensor.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Support for Zigbee binary sensors.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import PLATFORM_SCHEMA, XBeeDigitalIn, XBeeDigitalInConfig -from .const import CONF_ON_STATE, DOMAIN, STATES - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)}) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the XBee Zigbee binary sensor platform.""" - zigbee_device = hass.data[DOMAIN] - add_entities([XBeeBinarySensor(XBeeDigitalInConfig(config), zigbee_device)], True) - - -class XBeeBinarySensor(XBeeDigitalIn, BinarySensorEntity): - """Use XBeeDigitalIn as binary sensor.""" diff --git a/homeassistant/components/xbee/const.py b/homeassistant/components/xbee/const.py deleted file mode 100644 index a77e71e92f5..00000000000 --- a/homeassistant/components/xbee/const.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Constants for the xbee integration.""" -CONF_ON_STATE = "on_state" -DEFAULT_ON_STATE = "high" -DOMAIN = "xbee" -STATES = ["high", "low"] diff --git a/homeassistant/components/xbee/light.py b/homeassistant/components/xbee/light.py deleted file mode 100644 index 126eaf91f9d..00000000000 --- a/homeassistant/components/xbee/light.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Support for XBee Zigbee lights.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.light import ColorMode, LightEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig -from .const import CONF_ON_STATE, DEFAULT_ON_STATE, DOMAIN, STATES - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_ON_STATE, default=DEFAULT_ON_STATE): vol.In(STATES)} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Create and add an entity based on the configuration.""" - zigbee_device = hass.data[DOMAIN] - add_entities([XBeeLight(XBeeDigitalOutConfig(config), zigbee_device)]) - - -class XBeeLight(XBeeDigitalOut, LightEntity): - """Use XBeeDigitalOut as light.""" - - _attr_color_mode = ColorMode.ONOFF - _attr_supported_color_modes = {ColorMode.ONOFF} diff --git a/homeassistant/components/xbee/manifest.json b/homeassistant/components/xbee/manifest.json deleted file mode 100644 index 150036129d2..00000000000 --- a/homeassistant/components/xbee/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "disabled": "Integration library not compatible with Python 3.10", - "domain": "xbee", - "name": "XBee", - "documentation": "https://www.home-assistant.io/integrations/xbee", - "requirements": ["xbee-helper==0.0.7"], - "codeowners": [], - "iot_class": "local_polling", - "loggers": ["xbee_helper"] -} diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py deleted file mode 100644 index 9cea60ade8c..00000000000 --- a/homeassistant/components/xbee/sensor.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Support for XBee Zigbee sensors.""" -# pylint: disable=import-error -from __future__ import annotations - -from binascii import hexlify -import logging - -import voluptuous as vol -from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.const import CONF_TYPE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import DOMAIN, PLATFORM_SCHEMA, XBeeAnalogIn, XBeeAnalogInConfig, XBeeConfig - -_LOGGER = logging.getLogger(__name__) - -CONF_MAX_VOLTS = "max_volts" - -DEFAULT_VOLTS = 1.2 -TYPES = ["analog", "temperature"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In(TYPES), - vol.Optional(CONF_MAX_VOLTS, default=DEFAULT_VOLTS): vol.Coerce(float), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the XBee Zigbee platform. - - Uses the 'type' config value to work out which type of Zigbee sensor we're - dealing with and instantiates the relevant classes to handle it. - """ - zigbee_device = hass.data[DOMAIN] - typ = config[CONF_TYPE] - - try: - sensor_class, config_class = TYPE_CLASSES[typ] - except KeyError: - _LOGGER.exception("Unknown XBee Zigbee sensor type: %s", typ) - return - - add_entities([sensor_class(config_class(config), zigbee_device)], True) - - -class XBeeTemperatureSensor(SensorEntity): - """Representation of XBee Pro temperature sensor.""" - - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = TEMP_CELSIUS - - def __init__(self, config, device): - """Initialize the sensor.""" - self._config = config - self._device = device - self._temp = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._config.name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._temp - - def update(self): - """Get the latest data.""" - try: - self._temp = self._device.get_temperature(self._config.address) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get sample from " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - except ZigBeeException as exc: - _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) - - -# This must be below the classes to which it refers. -TYPE_CLASSES = { - "temperature": (XBeeTemperatureSensor, XBeeConfig), - "analog": (XBeeAnalogIn, XBeeAnalogInConfig), -} diff --git a/homeassistant/components/xbee/switch.py b/homeassistant/components/xbee/switch.py deleted file mode 100644 index 9cc25fbf7d2..00000000000 --- a/homeassistant/components/xbee/switch.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Support for XBee Zigbee switches.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig -from .const import CONF_ON_STATE, DOMAIN, STATES - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)}) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the XBee Zigbee switch platform.""" - zigbee_device = hass.data[DOMAIN] - add_entities([XBeeSwitch(XBeeDigitalOutConfig(config), zigbee_device)]) - - -class XBeeSwitch(XBeeDigitalOut, SwitchEntity): - """Representation of a XBee Zigbee Digital Out device.""" diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 19bbec8bbf5..c49fd55e8c8 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -21,6 +21,7 @@ from xbox.webapi.api.provider.smartglass.models import ( ) from homeassistant.components import application_credentials +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant @@ -74,9 +75,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET] ), ) + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( "Configuration of Xbox integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " + "will be removed in Home Assistant 2022.9.; Your existing configuration " "(including OAuth Application Credentials) has been imported into " "the UI automatically and can be safely removed from your " "configuration.yaml file" @@ -114,7 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "coordinator": coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -180,7 +190,7 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): name=DOMAIN, update_interval=timedelta(seconds=10), ) - self.data: XboxData = XboxData({}, []) + self.data: XboxData = XboxData({}, {}) self.client: XboxLiveClient = client self.consoles: SmartglassConsoleList = consoles @@ -230,7 +240,7 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): ) # Update user presence - presence_data = {} + presence_data: dict[str, PresenceData] = {} batch: PeopleResponse = await self.client.people.get_friends_own_batch( [self.client.xuid] ) @@ -262,7 +272,7 @@ def _build_presence_data(person: Person) -> PresenceData: online=person.presence_state == "Online", status=person.presence_text, in_party=person.multiplayer_summary.in_party > 0, - in_game=active_app and active_app.is_game, + in_game=active_app is not None and active_app.is_game, in_multiplayer=person.multiplayer_summary.in_multiplayer_session, gamer_score=person.gamer_score, gold_tenure=person.detail.tenure, diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 024feb294b5..5d0f3f92434 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -33,7 +33,7 @@ class XboxBaseSensorEntity(CoordinatorEntity[XboxUpdateCoordinator]): return self.coordinator.data.presence.get(self.xuid) @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the sensor.""" if not self.data: return None @@ -45,7 +45,7 @@ class XboxBaseSensorEntity(CoordinatorEntity[XboxUpdateCoordinator]): return f"{self.data.gamertag} {attr_name}" @property - def entity_picture(self) -> str: + def entity_picture(self) -> str | None: """Return the gamer pic.""" if not self.data: return None diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py index 7cf7ca6a6a5..ac97d502c55 100644 --- a/homeassistant/components/xbox/binary_sensor.py +++ b/homeassistant/components/xbox/binary_sensor.py @@ -54,7 +54,7 @@ def async_update_friends( current_ids = set(current) # Process new favorites, add them to Home Assistant - new_entities = [] + new_entities: list[XboxBinarySensorEntity] = [] for xuid in new_ids - current_ids: current[xuid] = [ XboxBinarySensorEntity(coordinator, xuid, attribute) @@ -75,7 +75,7 @@ def async_update_friends( async def async_remove_entities( xuid: str, coordinator: XboxUpdateCoordinator, - current: dict[str, XboxBinarySensorEntity], + current: dict[str, list[XboxBinarySensorEntity]], ) -> None: """Remove friend sensors from Home Assistant.""" registry = er.async_get(coordinator.hass) diff --git a/homeassistant/components/xbox/browse_media.py b/homeassistant/components/xbox/browse_media.py index b6e5a89efb3..ee1eabf1e00 100644 --- a/homeassistant/components/xbox/browse_media.py +++ b/homeassistant/components/xbox/browse_media.py @@ -1,7 +1,7 @@ """Support for media browsing.""" from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, SYSTEM_PFN_ID_MAP @@ -65,6 +65,8 @@ async def build_item_response( can_expand=True, children=[], ) + if TYPE_CHECKING: + assert library_info.children is not None # Add Home id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID @@ -84,7 +86,7 @@ async def build_item_response( title="Home", can_play=True, can_expand=False, - thumbnail=home_thumb.uri, + thumbnail=None if home_thumb is None else home_thumb.uri, ) ) @@ -107,7 +109,7 @@ async def build_item_response( title="Live TV", can_play=True, can_expand=False, - thumbnail=tv_thumb.uri, + thumbnail=None if tv_thumb is None else tv_thumb.uri, ) ) diff --git a/homeassistant/components/xbox/manifest.json b/homeassistant/components/xbox/manifest.json index 5adfa54a901..8857a55d66d 100644 --- a/homeassistant/components/xbox/manifest.json +++ b/homeassistant/components/xbox/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xbox", "requirements": ["xbox-webapi==2.0.11"], - "dependencies": ["auth", "application_credentials"], + "dependencies": ["auth", "application_credentials", "repairs"], "codeowners": ["@hunterjm"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index c6dae46a955..21b9b25ce2d 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -55,7 +55,7 @@ def async_parse_identifier( identifier = item.identifier or "" start = ["", "", ""] items = identifier.lstrip("/").split("~~", 2) - return tuple(items + start[len(items) :]) + return tuple(items + start[len(items) :]) # type: ignore[return-value] @dataclass @@ -201,7 +201,7 @@ class XboxSource(MediaSource): ) -def _build_game_item(item: InstalledPackage, images: list[Image]): +def _build_game_item(item: InstalledPackage, images: dict[str, list[Image]]): """Build individual game.""" thumbnail = "" image = _find_media_image(images.get(item.one_store_product_id, [])) diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index 02b4f8b84a4..9cba49d1dcb 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -56,7 +56,7 @@ def async_update_friends( current_ids = set(current) # Process new favorites, add them to Home Assistant - new_entities = [] + new_entities: list[XboxSensorEntity] = [] for xuid in new_ids - current_ids: current[xuid] = [ XboxSensorEntity(coordinator, xuid, attribute) @@ -77,7 +77,7 @@ def async_update_friends( async def async_remove_entities( xuid: str, coordinator: XboxUpdateCoordinator, - current: dict[str, XboxSensorEntity], + current: dict[str, list[XboxSensorEntity]], ) -> None: """Remove friend sensors from Home Assistant.""" registry = er.async_get(coordinator.hass) diff --git a/homeassistant/components/xbox/strings.json b/homeassistant/components/xbox/strings.json index accd6775941..68af0176fa8 100644 --- a/homeassistant/components/xbox/strings.json +++ b/homeassistant/components/xbox/strings.json @@ -13,5 +13,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Xbox YAML configuration is being removed", + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/xbox/translations/en.json b/homeassistant/components/xbox/translations/en.json index 0bb1266bded..2ef065af458 100644 --- a/homeassistant/components/xbox/translations/en.json +++ b/homeassistant/components/xbox/translations/en.json @@ -13,5 +13,11 @@ "title": "Pick Authentication Method" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Xbox YAML configuration is being removed" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/it.json b/homeassistant/components/xbox/translations/it.json index e60c37c9e5f..6cf5bf15bb9 100644 --- a/homeassistant/components/xbox/translations/it.json +++ b/homeassistant/components/xbox/translations/it.json @@ -13,5 +13,11 @@ "title": "Scegli il metodo di autenticazione" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Xbox in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Xbox verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index 7a9337c9332..2d1e95019b7 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/xbox/translations/pt-BR.json b/homeassistant/components/xbox/translations/pt-BR.json index 7f788c1ebb8..d1bb02e84dd 100644 --- a/homeassistant/components/xbox/translations/pt-BR.json +++ b/homeassistant/components/xbox/translations/pt-BR.json @@ -13,5 +13,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Xbox em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Xbox est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 0c9696a42ef..da3303494e5 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -196,7 +196,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: platforms = GATEWAY_PLATFORMS_NO_KEY - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) return True diff --git a/homeassistant/components/xiaomi_aqara/translations/pt.json b/homeassistant/components/xiaomi_aqara/translations/pt.json index a800e4d57c6..1b7f8e1c0b9 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt.json @@ -17,7 +17,7 @@ "data": { "name": "Nome da Gateway" }, - "description": "A chave (palavra-passe) pode ser recuperada usando este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se a chave n\u00e3o for fornecida, apenas os sensores estar\u00e3o acess\u00edveis" + "description": "A chave (palavra-passe) pode ser obtida usando este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se a chave n\u00e3o for fornecida, apenas os sensores estar\u00e3o acess\u00edveis" }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py new file mode 100644 index 00000000000..791ac1447ad --- /dev/null +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -0,0 +1,42 @@ +"""The Xiaomi Bluetooth integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Xiaomi BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py new file mode 100644 index 00000000000..a05e703db6a --- /dev/null +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -0,0 +1,299 @@ +"""Config flow for Xiaomi Bluetooth integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +import dataclasses +from typing import Any + +import voluptuous as vol +from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData +from xiaomi_ble.parser import EncryptionScheme + +from homeassistant.components import onboarding +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfo, + async_discovered_service_info, + async_process_advertisements, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +# How long to wait for additional advertisement packets if we don't have the right ones +ADDITIONAL_DISCOVERY_TIMEOUT = 60 + + +@dataclasses.dataclass +class Discovery: + """A discovered bluetooth device.""" + + title: str + discovery_info: BluetoothServiceInfo + device: DeviceData + + +def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: + return device.title or device.get_device_name() or discovery_info.name + + +class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Xiaomi Bluetooth.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, Discovery] = {} + + async def _async_wait_for_full_advertisement( + self, discovery_info: BluetoothServiceInfo, device: DeviceData + ) -> BluetoothServiceInfo: + """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" + if not device.pending: + return discovery_info + + def _process_more_advertisements( + service_info: BluetoothServiceInfo, + ) -> bool: + device.update(service_info) + return not device.pending + + return await async_process_advertisements( + self.hass, + _process_more_advertisements, + {"address": discovery_info.address}, + BluetoothScanningMode.ACTIVE, + ADDITIONAL_DISCOVERY_TIMEOUT, + ) + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + + title = _title(discovery_info, device) + self.context["title_placeholders"] = {"name": title} + + self._discovered_device = device + + # Wait until we have received enough information about this device to detect its encryption type + try: + self._discovery_info = await self._async_wait_for_full_advertisement( + discovery_info, device + ) + except asyncio.TimeoutError: + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() + + if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + return await self.async_step_get_encryption_key_legacy() + if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + return await self.async_step_get_encryption_key_4_5() + return await self.async_step_bluetooth_confirm() + + async def async_step_get_encryption_key_legacy( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Enter a legacy bindkey for a v2/v3 MiBeacon device.""" + assert self._discovery_info + assert self._discovered_device + + errors = {} + + if user_input is not None: + bindkey = user_input["bindkey"] + + if len(bindkey) != 24: + errors["bindkey"] = "expected_24_characters" + else: + self._discovered_device.bindkey = bytes.fromhex(bindkey) + + # If we got this far we already know supported will + # return true so we don't bother checking that again + # We just want to retry the decryption + self._discovered_device.supported(self._discovery_info) + + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) + + errors["bindkey"] = "decryption_failed" + + return self.async_show_form( + step_id="get_encryption_key_legacy", + description_placeholders=self.context["title_placeholders"], + data_schema=vol.Schema({vol.Required("bindkey"): vol.All(str, vol.Strip)}), + errors=errors, + ) + + async def async_step_get_encryption_key_4_5( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Enter a bindkey for a v4/v5 MiBeacon device.""" + assert self._discovery_info + assert self._discovered_device + + errors = {} + + if user_input is not None: + bindkey = user_input["bindkey"] + + if len(bindkey) != 32: + errors["bindkey"] = "expected_32_characters" + else: + self._discovered_device.bindkey = bytes.fromhex(bindkey) + + # If we got this far we already know supported will + # return true so we don't bother checking that again + # We just want to retry the decryption + self._discovered_device.supported(self._discovery_info) + + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) + + errors["bindkey"] = "decryption_failed" + + return self.async_show_form( + step_id="get_encryption_key_4_5", + description_placeholders=self.context["title_placeholders"], + data_schema=vol.Schema({vol.Required("bindkey"): vol.All(str, vol.Strip)}), + errors=errors, + ) + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + return self._async_get_or_create_entry() + + self._set_confirm_only() + return self.async_show_form( + step_id="bluetooth_confirm", + description_placeholders=self.context["title_placeholders"], + ) + + async def async_step_confirm_slow( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ack that device is slow.""" + if user_input is not None: + return self._async_get_or_create_entry() + + self._set_confirm_only() + return self.async_show_form( + step_id="confirm_slow", + description_placeholders=self.context["title_placeholders"], + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + discovery = self._discovered_devices[address] + + self.context["title_placeholders"] = {"name": discovery.title} + + # Wait until we have received enough information about this device to detect its encryption type + try: + self._discovery_info = await self._async_wait_for_full_advertisement( + discovery.discovery_info, discovery.device + ) + except asyncio.TimeoutError: + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() + + self._discovered_device = discovery.device + + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + return await self.async_step_get_encryption_key_legacy() + + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + return await self.async_step_get_encryption_key_4_5() + + return self._async_get_or_create_entry() + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = Discovery( + title=_title(discovery_info, device), + discovery_info=discovery_info, + device=device, + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + titles = { + address: discovery.title + for (address, discovery) in self._discovered_devices.items() + } + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a flow initialized by a reauth event.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry is not None + + device: DeviceData = entry_data["device"] + self._discovered_device = device + + self._discovery_info = device.last_service_info + + if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + return await self.async_step_get_encryption_key_legacy() + + if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + return await self.async_step_get_encryption_key_4_5() + + # Otherwise there wasn't actually encryption so abort + return self.async_abort(reason="reauth_successful") + + def _async_get_or_create_entry(self, bindkey=None): + data = {} + + if bindkey: + data["bindkey"] = bindkey + + if entry_id := self.context.get("entry_id"): + entry = self.hass.config_entries.async_get_entry(entry_id) + assert entry is not None + + self.hass.config_entries.async_update_entry(entry, data=data) + + # Reload the config entry to notify of updated config + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data=data, + ) diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py new file mode 100644 index 00000000000..9a38c75c05f --- /dev/null +++ b/homeassistant/components/xiaomi_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the Xiaomi Bluetooth integration.""" + +DOMAIN = "xiaomi_ble" diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json new file mode 100644 index 00000000000..a901439b2c9 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "xiaomi_ble", + "name": "Xiaomi BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", + "bluetooth": [ + { + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + } + ], + "requirements": ["xiaomi-ble==0.6.4"], + "dependencies": ["bluetooth"], + "codeowners": ["@Jc2k", "@Ernst79"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py new file mode 100644 index 00000000000..dcb95422609 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -0,0 +1,224 @@ +"""Support for xiaomi ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from xiaomi_ble import ( + DeviceClass, + DeviceKey, + SensorDeviceInfo, + SensorUpdate, + Units, + XiaomiBluetoothDeviceData, +) +from xiaomi_ble.parser import EncryptionScheme + +from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + CONDUCTIVITY, + ELECTRIC_POTENTIAL_VOLT, + LIGHT_LUX, + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( + key=f"{DeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( + key=str(Units.ELECTRIC_POTENTIAL_VOLT), + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + # Used for e.g. moisture sensor on HHCCJCY01 + (None, Units.PERCENTAGE): SensorEntityDescription( + key=str(Units.PERCENTAGE), + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for e.g. conductivity sensor on HHCCJCY01 + (None, Units.CONDUCTIVITY): SensorEntityDescription( + key=str(Units.CONDUCTIVITY), + device_class=None, + native_unit_of_measurement=CONDUCTIVITY, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for e.g. formaldehyde + (None, Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER): SensorEntityDescription( + key=str(Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER), + native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +def process_service_info( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + data: XiaomiBluetoothDeviceData, + service_info: BluetoothServiceInfoBleak, +) -> PassiveBluetoothDataUpdate: + """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + update = data.update(service_info) + + # If device isn't pending we know it has seen at least one broadcast with a payload + # If that payload was encrypted and the bindkey was not verified then we need to reauth + if ( + not data.pending + and data.encryption_scheme != EncryptionScheme.NONE + and not data.bindkey_verified + ): + entry.async_start_reauth(hass, data={"device": data}) + + return sensor_update_to_bluetooth_data_update(update) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Xiaomi BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + kwargs = {} + if bindkey := entry.data.get("bindkey"): + kwargs["bindkey"] = bytes.fromhex(bindkey) + data = XiaomiBluetoothDeviceData(**kwargs) + processor = PassiveBluetoothDataProcessor( + lambda service_info: process_service_info(hass, entry, data, service_info) + ) + entry.async_on_unload( + processor.async_add_entities_listener( + XiaomiBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class XiaomiBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a xiaomi ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json new file mode 100644 index 00000000000..5ecbb8e1b88 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + }, + "confirm_slow": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." + }, + "get_encryption_key_legacy": { + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", + "data": { + "bindkey": "Bindkey" + } + }, + "get_encryption_key_4_5": { + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 32 character hexadecimal bindkey.", + "data": { + "bindkey": "Bindkey" + } + } + }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, + "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "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%]" + } + } +} diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json new file mode 100644 index 00000000000..1c24ab7172e --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "decryption_failed": "La clau d'enlla\u00e7 proporcionada no ha funcionat, les dades del sensor no s'han pogut desxifrar. Comprova-la i torna-ho a provar.", + "expected_24_characters": "S'espera una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals.", + "expected_32_characters": "S'espera una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals.", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals." + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json new file mode 100644 index 00000000000..0d3e2c542d2 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "decryption_failed": "Der bereitgestellte Bindkey funktionierte nicht, Sensordaten konnten nicht entschl\u00fcsselt werden. Bitte \u00fcberpr\u00fcfe es und versuche es erneut.", + "expected_24_characters": "Erwartet wird ein 24-stelliger hexadezimaler Bindkey.", + "expected_32_characters": "Erwartet wird ein 32-stelliger hexadezimaler Bindkey.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindungsschl\u00fcssel" + }, + "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 32-stelligen hexadezimalen Bindungsschl\u00fcssel." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindungsschl\u00fcssel" + }, + "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindungsschl\u00fcssel." + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/el.json b/homeassistant/components/xiaomi_ble/translations/el.json new file mode 100644 index 00000000000..30dc61cb5dc --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/el.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "decryption_failed": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5, \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b1\u03bd \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "expected_24_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", + "expected_32_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03bf \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03ad\u03c3\u03bc\u03b5\u03c5\u03c3\u03b7\u03c2 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd." + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json new file mode 100644 index 00000000000..2cb77dd2c07 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "confirm_slow": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 32 character hexadecimal bindkey." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json new file mode 100644 index 00000000000..1895097e7b1 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "decryption_failed": "Esitatud sidumisv\u00f5ti ei t\u00f6\u00f6tanud, sensori andmeid ei saanud dekr\u00fcpteerida. Palun kontrolli seda ja proovi uuesti.", + "expected_24_characters": "Eeldati 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", + "expected_32_characters": "Eeldati 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", + "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Sidumisv\u00f5ti" + }, + "description": "Anduri edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks vajame 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Sidumisv\u00f5ti" + }, + "description": "Anduri edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks vajame 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/fr.json b/homeassistant/components/xiaomi_ble/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json new file mode 100644 index 00000000000..b03a41f60a6 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "expected_24_characters": "24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", + "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Kulcs (bindkey)" + }, + "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Kulcs (bindkey)" + }, + "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/it.json b/homeassistant/components/xiaomi_ble/translations/it.json new file mode 100644 index 00000000000..99adacee466 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/it.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controllare e riprovare.", + "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", + "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri.", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Chiave di collegamento" + }, + "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 32 caratteri." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Chiave di collegamento" + }, + "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 24 caratteri." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ja.json b/homeassistant/components/xiaomi_ble/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pl.json b/homeassistant/components/xiaomi_ble/translations/pl.json new file mode 100644 index 00000000000..2ca956019ef --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/pl.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", + "expected_24_characters": "Oczekiwano 24-znakowego szesnastkowego klucza bindkey.", + "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey.", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 32-znakowego szesnastkowego klucza bindkey." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 24-znakowego szesnastkowego klucza bindkey." + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json new file mode 100644 index 00000000000..21c251bf0eb --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", + "expected_24_characters": "Espera-se uma bindkey hexadecimal de 24 caracteres.", + "expected_32_characters": "Esperado um bindkey hexadecimal de 32 caracteres.", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma bindkey hexadecimal de 24 caracteres." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ru.json b/homeassistant/components/xiaomi_ble/translations/ru.json new file mode 100644 index 00000000000..a90da71d84e --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/ru.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "decryption_failed": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b, \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "expected_24_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", + "expected_32_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438" + }, + "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438" + }, + "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..81f7e2050af --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "decryption_failed": "\u6240\u63d0\u4f9b\u7684\u7d81\u5b9a\u78bc\u7121\u6cd5\u4f7f\u7528\u3001\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6cd5\u89e3\u5bc6\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "expected_24_characters": "\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", + "expected_32_characters": "\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "\u7d81\u5b9a\u78bc" + }, + "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "\u7d81\u5b9a\u78bc" + }, + "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index e2dc36ff568..9f0be1da528 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -18,6 +18,7 @@ from miio import ( CleaningDetails, CleaningSummary, ConsumableStatus, + Device as MiioDevice, DeviceException, DNDStatus, Fan, @@ -283,10 +284,10 @@ async def async_create_miio_device_and_coordinator( host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] name = entry.title - device = None + device: MiioDevice | None = None migrate = False update_method = _async_update_data_default - coordinator_class = DataUpdateCoordinator + coordinator_class: type[DataUpdateCoordinator] = DataUpdateCoordinator if ( model not in MODELS_HUMIDIFIER @@ -345,10 +346,13 @@ async def async_create_miio_device_and_coordinator( if migrate: # Removing fan platform entity for humidifiers and migrate the name to the config entry for migration entity_registry = er.async_get(hass) + assert entry.unique_id entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) if entity_id: # This check is entities that have a platform migration only and should be removed in the future - if migrate_entity_name := entity_registry.async_get(entity_id).name: + if (entity := entity_registry.async_get(entity_id)) and ( + migrate_entity_name := entity.name + ): hass.config_entries.async_update_entry(entry, title=migrate_entity_name) entity_registry.async_remove(entity_id) @@ -377,8 +381,10 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> name = entry.title gateway_id = entry.unique_id + assert gateway_id + # For backwards compat - if entry.unique_id.endswith("-gateway"): + if gateway_id.endswith("-gateway"): hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -419,7 +425,7 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> return async_update_data - coordinator_dict = {} + coordinator_dict: dict[str, DataUpdateCoordinator] = {} for sub_device in gateway.gateway_device.devices.values(): # Create update coordinator coordinator_dict[sub_device.sid] = DataUpdateCoordinator( @@ -436,10 +442,7 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> KEY_COORDINATOR: coordinator_dict, } - for platform in GATEWAY_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + await hass.config_entries.async_forward_entry_setups(entry, GATEWAY_PLATFORMS) async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -452,7 +455,7 @@ async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> b entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) return True diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 10d6c6129d3..30fcaa5152a 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" +from collections.abc import Callable import logging from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException @@ -218,7 +219,7 @@ class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity): return self._particulate_matter_10 -DEVICE_MAP = { +DEVICE_MAP: dict[str, dict[str, Callable]] = { MODEL_AIRQUALITYMONITOR_S1: { "device_class": AirQualityMonitor, "entity_class": AirMonitorS1, diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index d2f00e9805c..2b3dfde6190 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Xiaomi Miio binary sensors.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable from dataclasses import dataclass import logging @@ -57,13 +57,13 @@ class XiaomiMiioBinarySensorDescription(BinarySensorEntityDescription): BINARY_SENSOR_TYPES = ( XiaomiMiioBinarySensorDescription( key=ATTR_NO_WATER, - name="Water Tank Empty", + name="Water tank empty", icon="mdi:water-off-outline", entity_category=EntityCategory.DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_WATER_TANK_DETACHED, - name="Water Tank", + name="Water tank", icon="mdi:car-coolant-level", device_class=BinarySensorDeviceClass.CONNECTIVITY, value=lambda value: not value, @@ -71,13 +71,13 @@ BINARY_SENSOR_TYPES = ( ), XiaomiMiioBinarySensorDescription( key=ATTR_PTC_STATUS, - name="Auxiliary Heat Status", + name="Auxiliary heat status", device_class=BinarySensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_POWERSUPPLY_ATTACHED, - name="Power Supply", + name="Power supply", device_class=BinarySensorDeviceClass.PLUG, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -89,7 +89,7 @@ FAN_ZA5_BINARY_SENSORS = (ATTR_POWERSUPPLY_ATTACHED,) VACUUM_SENSORS = { ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_BOX_ATTACHED, - name="Mop Attached", + name="Mop attached", icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -98,7 +98,7 @@ VACUUM_SENSORS = { ), ATTR_WATER_BOX_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_BOX_ATTACHED, - name="Water Box Attached", + name="Water box attached", icon="mdi:water", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -107,7 +107,7 @@ VACUUM_SENSORS = { ), ATTR_WATER_SHORTAGE: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_SHORTAGE, - name="Water Shortage", + name="Water shortage", icon="mdi:water", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -120,7 +120,7 @@ VACUUM_SENSORS_SEPARATE_MOP = { **VACUUM_SENSORS, ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_MOP_ATTACHED, - name="Mop Attached", + name="Mop attached", icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -158,7 +158,6 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): continue entities.append( XiaomiGenericBinarySensor( - f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", @@ -180,7 +179,7 @@ async def async_setup_entry( if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: model = config_entry.data[CONF_MODEL] - sensors = [] + sensors: Iterable[str] = [] if model in MODEL_AIRFRESH_A1 or model in MODEL_AIRFRESH_T2017: sensors = AIRFRESH_A1_BINARY_SENSORS elif model in MODEL_FAN_ZA5: @@ -199,7 +198,6 @@ async def async_setup_entry( continue entities.append( XiaomiGenericBinarySensor( - f"{config_entry.title} {description.name}", hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], config_entry, f"{description.key}_{config_entry.unique_id}", @@ -216,9 +214,9 @@ class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity) entity_description: XiaomiMiioBinarySensorDescription - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the entity.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self.entity_description = description self._attr_entity_registry_enabled_default = ( diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py index 0f5b59a262d..e02e6ad81bf 100644 --- a/homeassistant/components/xiaomi_miio/button.py +++ b/homeassistant/components/xiaomi_miio/button.py @@ -38,7 +38,7 @@ class XiaomiMiioButtonDescription(ButtonEntityDescription): BUTTON_TYPES = ( XiaomiMiioButtonDescription( key=ATTR_RESET_DUST_FILTER, - name="Reset Dust Filter", + name="Reset dust filter", icon="mdi:air-filter", method_press="reset_dust_filter", method_press_error_message="Resetting the dust filter lifetime failed", @@ -46,7 +46,7 @@ BUTTON_TYPES = ( ), XiaomiMiioButtonDescription( key=ATTR_RESET_UPPER_FILTER, - name="Reset Upper Filter", + name="Reset upper filter", icon="mdi:air-filter", method_press="reset_upper_filter", method_press_error_message="Resetting the upper filter lifetime failed.", @@ -86,7 +86,6 @@ async def async_setup_entry( entities.append( XiaomiGenericCoordinatedButton( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{unique_id}", @@ -105,9 +104,9 @@ class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.RESTART - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self.entity_description = description async def async_press(self) -> None: diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index dc1cf86f7df..81ca71d6b68 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -110,7 +110,9 @@ class XiaomiMiioEntity(Entity): class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): """Representation of a base a coordinated Xiaomi Miio Entity.""" - def __init__(self, name, device, entry, unique_id, coordinator): + _attr_has_entity_name = True + + def __init__(self, device, entry, unique_id, coordinator): """Initialize the coordinated Xiaomi Miio Device.""" super().__init__(coordinator) self._device = device @@ -119,18 +121,12 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): self._device_id = entry.unique_id self._device_name = entry.title self._unique_id = unique_id - self._name = name @property def unique_id(self): """Return an unique ID.""" return self._unique_id - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name - @property def device_info(self) -> DeviceInfo: """Return the device info.""" @@ -184,9 +180,9 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): return int(timedelta.total_seconds()) @staticmethod - def _parse_datetime_time(time: datetime.time) -> str: + def _parse_datetime_time(initial_time: datetime.time) -> str: time = datetime.datetime.now().replace( - hour=time.hour, minute=time.minute, second=0, microsecond=0 + hour=initial_time.hour, minute=initial_time.minute, second=0, microsecond=0 ) if time < datetime.datetime.now(): diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index 3f819d7ab7d..1ec914fc647 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_TOKEN @@ -18,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index ac85955b347..aa4b8a8a1bc 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -7,14 +7,22 @@ import logging import math from typing import Any -from miio.airfresh import OperationMode as AirfreshOperationMode -from miio.airfresh_t2017 import OperationMode as AirfreshOperationModeT2017 -from miio.airpurifier import OperationMode as AirpurifierOperationMode -from miio.airpurifier_miot import OperationMode as AirpurifierMiotOperationMode from miio.fan_common import ( MoveDirection as FanMoveDirection, OperationMode as FanOperationMode, ) +from miio.integrations.airpurifier.dmaker.airfresh_t2017 import ( + OperationMode as AirfreshOperationModeT2017, +) +from miio.integrations.airpurifier.zhimi.airfresh import ( + OperationMode as AirfreshOperationMode, +) +from miio.integrations.airpurifier.zhimi.airpurifier import ( + OperationMode as AirpurifierOperationMode, +) +from miio.integrations.airpurifier.zhimi.airpurifier_miot import ( + OperationMode as AirpurifierMiotOperationMode, +) from miio.integrations.fan.zhimi.zhimi_miot import ( OperationModeFanZA5 as FanZA5OperationMode, ) @@ -183,14 +191,14 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fan from a config entry.""" - entities = [] + entities: list[FanEntity] = [] + entity: FanEntity if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return hass.data.setdefault(DATA_KEY, {}) - name = config_entry.title model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -198,7 +206,6 @@ async def async_setup_entry( if model == MODEL_AIRPURIFIER_3C: entity = XiaomiAirPurifierMB4( - name, device, config_entry, unique_id, @@ -206,28 +213,27 @@ async def async_setup_entry( ) elif model in MODELS_PURIFIER_MIOT: entity = XiaomiAirPurifierMiot( - name, device, config_entry, unique_id, coordinator, ) elif model.startswith("zhimi.airpurifier."): - entity = XiaomiAirPurifier(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirPurifier(device, config_entry, unique_id, coordinator) elif model.startswith("zhimi.airfresh."): - entity = XiaomiAirFresh(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirFresh(device, config_entry, unique_id, coordinator) elif model == MODEL_AIRFRESH_A1: - entity = XiaomiAirFreshA1(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirFreshA1(device, config_entry, unique_id, coordinator) elif model == MODEL_AIRFRESH_T2017: - entity = XiaomiAirFreshT2017(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirFreshT2017(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_P5: - entity = XiaomiFanP5(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFanP5(device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIIO: - entity = XiaomiFan(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFan(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_ZA5: - entity = XiaomiFanZA5(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIOT: - entity = XiaomiFanMiot(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator) else: return @@ -276,9 +282,9 @@ async def async_setup_entry( class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._available_attributes = {} self._state = None @@ -299,7 +305,7 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): return self._preset_modes @property - def percentage(self) -> None: + def percentage(self) -> int | None: """Return the percentage based speed of the fan.""" return None @@ -348,9 +354,9 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): class XiaomiGenericAirPurifier(XiaomiGenericDevice): """Representation of a generic AirPurifier device.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic AirPurifier device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._speed_count = 100 @@ -395,9 +401,9 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO @@ -564,9 +570,9 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): """Representation of a Xiaomi Air Purifier MB4.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize Air Purifier MB4.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRPURIFIER_3C self._preset_modes = PRESET_MODES_AIRPURIFIER_3C @@ -618,9 +624,9 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): "Interval": AirfreshOperationMode.Interval, } - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH @@ -716,9 +722,9 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): class XiaomiAirFreshA1(XiaomiGenericAirPurifier): """Representation of a Xiaomi Air Fresh A1.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._favorite_speed = None self._device_features = FEATURE_FLAGS_AIRFRESH_A1 self._preset_modes = PRESET_MODES_AIRFRESH_A1 @@ -791,9 +797,9 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): class XiaomiAirFreshT2017(XiaomiAirFreshA1): """Representation of a Xiaomi Air Fresh T2017.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRFRESH_T2017 self._speed_range = (60, 300) @@ -801,9 +807,9 @@ class XiaomiAirFreshT2017(XiaomiAirFreshA1): class XiaomiGenericFan(XiaomiGenericDevice): """Representation of a generic Xiaomi Fan.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) if self._model == MODEL_FAN_P5: self._device_features = FEATURE_FLAGS_FAN_P5 @@ -876,9 +882,9 @@ class XiaomiGenericFan(XiaomiGenericDevice): class XiaomiFan(XiaomiGenericFan): """Representation of a Xiaomi Fan.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._state = self.coordinator.data.is_on self._oscillating = self.coordinator.data.oscillate @@ -967,9 +973,9 @@ class XiaomiFan(XiaomiGenericFan): class XiaomiFanP5(XiaomiGenericFan): """Representation of a Xiaomi Fan P5.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._state = self.coordinator.data.is_on self._preset_mode = self.coordinator.data.mode.name diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index d5c829754d6..b5a5e738ea0 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -2,9 +2,15 @@ import logging import math -from miio.airhumidifier import OperationMode as AirhumidifierOperationMode -from miio.airhumidifier_miot import OperationMode as AirhumidifierMiotOperationMode -from miio.airhumidifier_mjjsq import OperationMode as AirhumidifierMjjsqOperationMode +from miio.integrations.humidifier.deerma.airhumidifier_mjjsq import ( + OperationMode as AirhumidifierMjjsqOperationMode, +) +from miio.integrations.humidifier.zhimi.airhumidifier import ( + OperationMode as AirhumidifierOperationMode, +) +from miio.integrations.humidifier.zhimi.airhumidifier_miot import ( + OperationMode as AirhumidifierMiotOperationMode, +) from homeassistant.components.humidifier import ( HumidifierDeviceClass, @@ -68,16 +74,15 @@ async def async_setup_entry( if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return - entities = [] + entities: list[HumidifierEntity] = [] + entity: HumidifierEntity model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - name = config_entry.title if model in MODELS_HUMIDIFIER_MIOT: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifierMiot( - name, air_humidifier, config_entry, unique_id, @@ -86,7 +91,6 @@ async def async_setup_entry( elif model in MODELS_HUMIDIFIER_MJJSQ: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifierMjjsq( - name, air_humidifier, config_entry, unique_id, @@ -95,7 +99,6 @@ async def async_setup_entry( else: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifier( - name, air_humidifier, config_entry, unique_id, @@ -112,10 +115,11 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): _attr_device_class = HumidifierDeviceClass.HUMIDIFIER _attr_supported_features = HumidifierEntityFeature.MODES + supported_features: int - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" - super().__init__(name, device, entry, unique_id, coordinator=coordinator) + super().__init__(device, entry, unique_id, coordinator=coordinator) self._state = None self._attributes = {} @@ -169,9 +173,11 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): """Representation of a Xiaomi Air Humidifier.""" - def __init__(self, name, device, entry, unique_id, coordinator): + available_modes: list[str] + + def __init__(self, device, entry, unique_id, coordinator): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_min_humidity = 30 self._attr_max_humidity = 80 diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 28feb23d93e..fd6af0d9560 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -1,4 +1,6 @@ """Support for Xiaomi Philips Lights.""" +from __future__ import annotations + import asyncio import datetime from datetime import timedelta @@ -6,7 +8,14 @@ from functools import partial import logging from math import ceil -from miio import Ceil, DeviceException, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight +from miio import ( + Ceil, + Device as MiioDevice, + DeviceException, + PhilipsBulb, + PhilipsEyecare, + PhilipsMoonlight, +) from miio.gateway.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, @@ -115,7 +124,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi light from a config entry.""" - entities = [] + entities: list[LightEntity] = [] + entity: LightEntity + light: MiioDevice if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] @@ -195,7 +206,7 @@ async def async_setup_entry( async def async_service_handler(service: ServiceCall) -> None: """Map services to methods on Xiaomi Philips Lights.""" - method = SERVICE_TO_METHOD.get(service.service) + method = SERVICE_TO_METHOD[service.service] params = { key: value for key, value in service.data.items() @@ -372,7 +383,7 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): @staticmethod def delayed_turn_off_timestamp( - countdown: int, current: datetime, previous: datetime + countdown: int, current: datetime.datetime, previous: datetime.datetime ): """Update the turn off timestamp only if necessary.""" if countdown is not None and countdown > 0: @@ -671,7 +682,7 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): @staticmethod def delayed_turn_off_timestamp( - countdown: int, current: datetime, previous: datetime + countdown: int, current: datetime.datetime, previous: datetime.datetime ): """Update the turn off timestamp only if necessary.""" if countdown is not None and countdown > 0: @@ -783,7 +794,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): return 588 @property - def hs_color(self) -> tuple: + def hs_color(self) -> tuple[float, float] | None: """Return the hs color value.""" return self._hs_color diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 7157e32299a..0f1a9dd92aa 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.11"], + "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.12"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling", diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 7fd5347f432..e7c61044e25 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -107,7 +107,7 @@ class OscillationAngleValues: NUMBER_TYPES = { FEATURE_SET_MOTOR_SPEED: XiaomiMiioNumberDescription( key=ATTR_MOTOR_SPEED, - name="Motor Speed", + name="Motor speed", icon="mdi:fast-forward-outline", native_unit_of_measurement="rpm", native_min_value=200, @@ -119,7 +119,7 @@ NUMBER_TYPES = { ), FEATURE_SET_FAVORITE_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_LEVEL, - name="Favorite Level", + name="Favorite level", icon="mdi:star-cog", native_min_value=0, native_max_value=17, @@ -129,7 +129,7 @@ NUMBER_TYPES = { ), FEATURE_SET_FAN_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAN_LEVEL, - name="Fan Level", + name="Fan level", icon="mdi:fan", native_min_value=1, native_max_value=3, @@ -149,7 +149,7 @@ NUMBER_TYPES = { ), FEATURE_SET_OSCILLATION_ANGLE: XiaomiMiioNumberDescription( key=ATTR_OSCILLATION_ANGLE, - name="Oscillation Angle", + name="Oscillation angle", icon="mdi:angle-acute", native_unit_of_measurement=DEGREE, native_min_value=1, @@ -160,7 +160,7 @@ NUMBER_TYPES = { ), FEATURE_SET_DELAY_OFF_COUNTDOWN: XiaomiMiioNumberDescription( key=ATTR_DELAY_OFF_COUNTDOWN, - name="Delay Off Countdown", + name="Delay off countdown", icon="mdi:fan-off", native_unit_of_measurement=TIME_MINUTES, native_min_value=0, @@ -171,7 +171,7 @@ NUMBER_TYPES = { ), FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS, - name="Led Brightness", + name="LED brightness", icon="mdi:brightness-6", native_min_value=0, native_max_value=100, @@ -181,7 +181,7 @@ NUMBER_TYPES = { ), FEATURE_SET_LED_BRIGHTNESS_LEVEL: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS_LEVEL, - name="Led Brightness", + name="LED brightness", icon="mdi:brightness-6", native_min_value=0, native_max_value=8, @@ -191,7 +191,7 @@ NUMBER_TYPES = { ), FEATURE_SET_FAVORITE_RPM: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_RPM, - name="Favorite Motor Speed", + name="Favorite motor speed", icon="mdi:star-cog", native_unit_of_measurement="rpm", native_min_value=300, @@ -283,7 +283,6 @@ async def async_setup_entry( entities.append( XiaomiNumberEntity( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{config_entry.unique_id}", @@ -298,9 +297,9 @@ async def async_setup_entry( class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the generic Xiaomi attribute selector.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_native_value = self._extract_value_from_attribute( coordinator.data, description.key diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index f2fc736ed82..b7e6a65775e 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -3,12 +3,22 @@ from __future__ import annotations from dataclasses import dataclass -from miio.airfresh import LedBrightness as AirfreshLedBrightness -from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness -from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness -from miio.airpurifier import LedBrightness as AirpurifierLedBrightness -from miio.airpurifier_miot import LedBrightness as AirpurifierMiotLedBrightness from miio.fan_common import LedBrightness as FanLedBrightness +from miio.integrations.airpurifier.zhimi.airfresh import ( + LedBrightness as AirfreshLedBrightness, +) +from miio.integrations.airpurifier.zhimi.airpurifier import ( + LedBrightness as AirpurifierLedBrightness, +) +from miio.integrations.airpurifier.zhimi.airpurifier_miot import ( + LedBrightness as AirpurifierMiotLedBrightness, +) +from miio.integrations.humidifier.zhimi.airhumidifier import ( + LedBrightness as AirhumidifierLedBrightness, +) +from miio.integrations.humidifier.zhimi.airhumidifier_miot import ( + LedBrightness as AirhumidifierMiotLedBrightness, +) from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -110,7 +120,6 @@ async def async_setup_entry( description = SELECTOR_TYPES[FEATURE_SET_LED_BRIGHTNESS] entities.append( entity_class( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{config_entry.unique_id}", @@ -125,9 +134,9 @@ async def async_setup_entry( class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the generic Xiaomi attribute selector.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_options = list(description.options) self.entity_description = description @@ -135,9 +144,9 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): class XiaomiAirHumidifierSelector(XiaomiSelector): """Representation of a Xiaomi Air Humidifier selector.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator, description) + super().__init__(device, entry, unique_id, coordinator, description) self._current_led_brightness = self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index df0c953d62a..235d103b53d 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,6 +1,7 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5) and Humidifier.""" from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass import logging @@ -171,13 +172,13 @@ SENSOR_TYPES = { ), ATTR_LOAD_POWER: XiaomiMiioSensorDescription( key=ATTR_LOAD_POWER, - name="Load Power", + name="Load power", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, ), ATTR_WATER_LEVEL: XiaomiMiioSensorDescription( key=ATTR_WATER_LEVEL, - name="Water Level", + name="Water level", native_unit_of_measurement=PERCENTAGE, icon="mdi:water-check", state_class=SensorStateClass.MEASUREMENT, @@ -185,7 +186,7 @@ SENSOR_TYPES = { ), ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( key=ATTR_ACTUAL_SPEED, - name="Actual Speed", + name="Actual speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -193,7 +194,7 @@ SENSOR_TYPES = { ), ATTR_CONTROL_SPEED: XiaomiMiioSensorDescription( key=ATTR_CONTROL_SPEED, - name="Control Speed", + name="Control speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -201,7 +202,7 @@ SENSOR_TYPES = { ), ATTR_FAVORITE_SPEED: XiaomiMiioSensorDescription( key=ATTR_FAVORITE_SPEED, - name="Favorite Speed", + name="Favorite speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -209,7 +210,7 @@ SENSOR_TYPES = { ), ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR_SPEED, - name="Motor Speed", + name="Motor speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -217,7 +218,7 @@ SENSOR_TYPES = { ), ATTR_MOTOR2_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR2_SPEED, - name="Second Motor Speed", + name="Second motor speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -225,7 +226,7 @@ SENSOR_TYPES = { ), ATTR_USE_TIME: XiaomiMiioSensorDescription( key=ATTR_USE_TIME, - name="Use Time", + name="Use time", native_unit_of_measurement=TIME_SECONDS, icon="mdi:progress-clock", state_class=SensorStateClass.TOTAL_INCREASING, @@ -268,7 +269,7 @@ SENSOR_TYPES = { ), ATTR_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_FILTER_LIFE_REMAINING, - name="Filter Life Remaining", + name="Filter life remaining", native_unit_of_measurement=PERCENTAGE, icon="mdi:air-filter", state_class=SensorStateClass.MEASUREMENT, @@ -277,7 +278,7 @@ SENSOR_TYPES = { ), ATTR_FILTER_USE: XiaomiMiioSensorDescription( key=ATTR_FILTER_HOURS_USED, - name="Filter Use", + name="Filter use", native_unit_of_measurement=TIME_HOURS, icon="mdi:clock-outline", state_class=SensorStateClass.MEASUREMENT, @@ -319,14 +320,14 @@ SENSOR_TYPES = { ), ATTR_CARBON_DIOXIDE: XiaomiMiioSensorDescription( key=ATTR_CARBON_DIOXIDE, - name="Carbon Dioxide", + name="Carbon dioxide", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), ATTR_PURIFY_VOLUME: XiaomiMiioSensorDescription( key=ATTR_PURIFY_VOLUME, - name="Purify Volume", + name="Purify volume", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, @@ -470,7 +471,7 @@ FAN_V2_V3_SENSORS = ( FAN_ZA5_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) -MODEL_TO_SENSORS_MAP = { +MODEL_TO_SENSORS_MAP: dict[str, tuple[str, ...]] = { MODEL_AIRFRESH_A1: AIRFRESH_SENSORS_A1, MODEL_AIRFRESH_VA2: AIRFRESH_SENSORS, MODEL_AIRFRESH_T2017: AIRFRESH_SENSORS_T2017, @@ -490,7 +491,7 @@ VACUUM_SENSORS = { f"dnd_{ATTR_DND_START}": XiaomiMiioSensorDescription( key=ATTR_DND_START, icon="mdi:minus-circle-off", - name="DnD Start", + name="DnD start", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, @@ -499,7 +500,7 @@ VACUUM_SENSORS = { f"dnd_{ATTR_DND_END}": XiaomiMiioSensorDescription( key=ATTR_DND_END, icon="mdi:minus-circle-off", - name="DnD End", + name="DnD end", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, @@ -508,7 +509,7 @@ VACUUM_SENSORS = { f"last_clean_{ATTR_LAST_CLEAN_START}": XiaomiMiioSensorDescription( key=ATTR_LAST_CLEAN_START, icon="mdi:clock-time-twelve", - name="Last Clean Start", + name="Last clean start", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, entity_category=EntityCategory.DIAGNOSTIC, @@ -518,7 +519,7 @@ VACUUM_SENSORS = { icon="mdi:clock-time-twelve", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - name="Last Clean End", + name="Last clean end", entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_TIME}": XiaomiMiioSensorDescription( @@ -526,7 +527,7 @@ VACUUM_SENSORS = { icon="mdi:timer-sand", key=ATTR_LAST_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - name="Last Clean Duration", + name="Last clean duration", entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( @@ -534,7 +535,7 @@ VACUUM_SENSORS = { icon="mdi:texture-box", key=ATTR_LAST_CLEAN_AREA, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - name="Last Clean Area", + name="Last clean area", entity_category=EntityCategory.DIAGNOSTIC, ), f"current_{ATTR_STATUS_CLEAN_TIME}": XiaomiMiioSensorDescription( @@ -542,7 +543,7 @@ VACUUM_SENSORS = { icon="mdi:timer-sand", key=ATTR_STATUS_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.status, - name="Current Clean Duration", + name="Current clean duration", entity_category=EntityCategory.DIAGNOSTIC, ), f"current_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( @@ -551,7 +552,7 @@ VACUUM_SENSORS = { key=ATTR_STATUS_CLEAN_AREA, parent_key=VacuumCoordinatorDataAttributes.status, entity_category=EntityCategory.DIAGNOSTIC, - name="Current Clean Area", + name="Current clean area", ), f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_DURATION}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -567,7 +568,7 @@ VACUUM_SENSORS = { icon="mdi:texture-box", key=ATTR_CLEAN_HISTORY_TOTAL_AREA, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, - name="Total Clean Area", + name="Total clean area", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -577,17 +578,17 @@ VACUUM_SENSORS = { state_class=SensorStateClass.TOTAL_INCREASING, key=ATTR_CLEAN_HISTORY_COUNT, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, - name="Total Clean Count", + name="Total clean count", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT}": XiaomiMiioSensorDescription( native_unit_of_measurement="", icon="mdi:counter", - state_class="total_increasing", + state_class=SensorStateClass.TOTAL_INCREASING, key=ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, - name="Total Dust Collection Count", + name="Total dust collection count", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -596,7 +597,7 @@ VACUUM_SENSORS = { icon="mdi:brush", key=ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Main Brush Left", + name="Main brush left", entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT}": XiaomiMiioSensorDescription( @@ -604,7 +605,7 @@ VACUUM_SENSORS = { icon="mdi:brush", key=ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Side Brush Left", + name="Side brush left", entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_FILTER_LEFT}": XiaomiMiioSensorDescription( @@ -612,7 +613,7 @@ VACUUM_SENSORS = { icon="mdi:air-filter", key=ATTR_CONSUMABLE_STATUS_FILTER_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Filter Left", + name="Filter left", entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT}": XiaomiMiioSensorDescription( @@ -620,7 +621,7 @@ VACUUM_SENSORS = { icon="mdi:eye-outline", key=ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Sensor Dirty Left", + name="Sensor dirty left", entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -643,7 +644,6 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): continue entities.append( XiaomiGenericSensor( - f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", @@ -661,7 +661,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi sensor from a config entry.""" - entities = [] + entities: list[SensorEntity] = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] @@ -715,7 +715,7 @@ async def async_setup_entry( ) else: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - sensors = [] + sensors: Iterable[str] = [] if model in MODEL_TO_SENSORS_MAP: sensors = MODEL_TO_SENSORS_MAP[model] elif model in MODELS_HUMIDIFIER_MIOT: @@ -740,7 +740,6 @@ async def async_setup_entry( continue entities.append( XiaomiGenericSensor( - f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", @@ -757,9 +756,9 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): entity_description: XiaomiMiioSensorDescription - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the entity.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self.entity_description = description self._attr_unique_id = unique_id self._attr_native_value = self._determine_native_value() diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 05d6543e93d..f80b6343d09 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -201,12 +201,20 @@ MODEL_TO_FEATURES_MAP = { @dataclass -class XiaomiMiioSwitchDescription(SwitchEntityDescription): +class XiaomiMiioSwitchRequiredKeyMixin: + """A class that describes switch entities.""" + + feature: int + method_on: str + method_off: str + + +@dataclass +class XiaomiMiioSwitchDescription( + SwitchEntityDescription, XiaomiMiioSwitchRequiredKeyMixin +): """A class that describes switch entities.""" - feature: int | None = None - method_on: str | None = None - method_off: str | None = None available_with_device_off: bool = True @@ -223,7 +231,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_CHILD_LOCK, feature=FEATURE_SET_CHILD_LOCK, - name="Child Lock", + name="Child lock", icon="mdi:lock", method_on="async_set_child_lock_on", method_off="async_set_child_lock_off", @@ -241,7 +249,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_DRY, feature=FEATURE_SET_DRY, - name="Dry Mode", + name="Dry mode", icon="mdi:hair-dryer", method_on="async_set_dry_on", method_off="async_set_dry_off", @@ -250,7 +258,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_CLEAN, feature=FEATURE_SET_CLEAN, - name="Clean Mode", + name="Clean mode", icon="mdi:shimmer", method_on="async_set_clean_on", method_off="async_set_clean_off", @@ -260,7 +268,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_LED, feature=FEATURE_SET_LED, - name="Led", + name="LED", icon="mdi:led-outline", method_on="async_set_led_on", method_off="async_set_led_off", @@ -269,7 +277,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_LEARN_MODE, feature=FEATURE_SET_LEARN_MODE, - name="Learn Mode", + name="Learn mode", icon="mdi:school-outline", method_on="async_set_learn_mode_on", method_off="async_set_learn_mode_off", @@ -278,7 +286,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_AUTO_DETECT, feature=FEATURE_SET_AUTO_DETECT, - name="Auto Detect", + name="Auto detect", method_on="async_set_auto_detect_on", method_off="async_set_auto_detect_off", entity_category=EntityCategory.CONFIG, @@ -295,7 +303,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_PTC, feature=FEATURE_SET_PTC, - name="Auxiliary Heat", + name="Auxiliary heat", icon="mdi:radiator", method_on="async_set_ptc_on", method_off="async_set_ptc_off", @@ -345,7 +353,6 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): if description.feature & device_features: entities.append( XiaomiGenericCoordinatedSwitch( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{unique_id}", @@ -443,7 +450,7 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): async def async_service_handler(service: ServiceCall) -> None: """Map services to methods on XiaomiPlugGenericSwitch.""" - method = SERVICE_TO_METHOD.get(service.service) + method = SERVICE_TO_METHOD[service.service] params = { key: value for key, value in service.data.items() @@ -480,9 +487,11 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Representation of a Xiaomi Plug Generic.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + entity_description: XiaomiMiioSwitchDescription + + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_is_on = self._extract_value_from_attribute( self.coordinator.data, description.key diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 51f093b871c..a7536283240 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -9,7 +9,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rj\u00fck, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got", + "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rem, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got", "cloud_login_error": "Nem siker\u00fclt bejelentkezni a Xioami Miio Cloud szolg\u00e1ltat\u00e1sba, ellen\u0151rizze a hiteles\u00edt\u0151 adatokat.", "cloud_no_devices": "Nincs eszk\u00f6z ebben a Xiaomi Miio felh\u0151fi\u00f3kban.", "unknown_device": "Az eszk\u00f6z modell nem ismert, nem tudja be\u00e1ll\u00edtani az eszk\u00f6zt a konfigur\u00e1ci\u00f3s folyamat seg\u00edts\u00e9g\u00e9vel.", @@ -52,7 +52,7 @@ }, "options": { "error": { - "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rj\u00fck, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got" + "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rem, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got" }, "step": { "init": { diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 0877850f754..672a338f082 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -36,11 +36,11 @@ "host": "IP\u30a2\u30c9\u30ec\u30b9", "token": "API\u30c8\u30fc\u30af\u30f3" }, - "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002" + "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u7d71\u5408\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002" }, "reauth_confirm": { - "description": "Xiaomi Miio\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Xiaomi Miio\u7d71\u5408\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/pt.json b/homeassistant/components/xiaomi_miio/translations/pt.json index db0e0c2a137..41cb2e55e91 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt.json +++ b/homeassistant/components/xiaomi_miio/translations/pt.json @@ -5,6 +5,14 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "manual": { + "data": { + "host": "Endere\u00e7o IP", + "token": "API Token" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.pt.json b/homeassistant/components/xiaomi_miio/translations/select.pt.json new file mode 100644 index 00000000000..24ed8a3e752 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "dim": "Escurecer" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 6d398ff40b9..e8f4b334544 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -86,11 +86,9 @@ async def async_setup_entry( entities = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: - name = config_entry.title unique_id = config_entry.unique_id mirobo = MiroboVacuum( - name, hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], config_entry, unique_id, @@ -201,14 +199,13 @@ class MiroboVacuum( def __init__( self, - name, device, entry, unique_id, coordinator: DataUpdateCoordinator[VacuumCoordinatorData], ): """Initialize the Xiaomi vacuum cleaner robot handler.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._state: str | None = None async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index 8712241a2aa..763742cce70 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) return True diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index fbd3f945aa2..e2df1b09ebe 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -14,7 +14,6 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -47,7 +46,6 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: """Initialize the Yale Alarm Device.""" super().__init__(coordinator) - self._attr_name = coordinator.entry.data[CONF_NAME] self._attr_unique_id = coordinator.entry.entry_id async def async_alarm_disarm(self, code: str | None = None) -> None: diff --git a/homeassistant/components/yale_smart_alarm/binary_sensor.py b/homeassistant/components/yale_smart_alarm/binary_sensor.py index 566dbed8c33..635fb3fad60 100644 --- a/homeassistant/components/yale_smart_alarm/binary_sensor.py +++ b/homeassistant/components/yale_smart_alarm/binary_sensor.py @@ -7,7 +7,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -21,7 +20,7 @@ SENSOR_TYPES = ( key="acfail", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, - name="Power Loss", + name="Power loss", ), BinarySensorEntityDescription( key="battery", @@ -85,9 +84,6 @@ class YaleProblemSensor(YaleAlarmEntity, BinarySensorEntity): """Initiate Yale Problem Sensor.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = ( - f"{coordinator.entry.data[CONF_NAME]} {entity_description.name}" - ) self._attr_unique_id = f"{coordinator.entry.entry_id}-{entity_description.key}" @property diff --git a/homeassistant/components/yale_smart_alarm/button.py b/homeassistant/components/yale_smart_alarm/button.py index cd312e79ceb..d8601ec85f9 100644 --- a/homeassistant/components/yale_smart_alarm/button.py +++ b/homeassistant/components/yale_smart_alarm/button.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,7 +13,11 @@ from .coordinator import YaleDataUpdateCoordinator from .entity import YaleAlarmEntity BUTTON_TYPES = ( - ButtonEntityDescription(key="panic", name="Panic Button", icon="mdi:alarm-light"), + ButtonEntityDescription( + key="panic", + name="Panic button", + icon="mdi:alarm-light", + ), ) @@ -47,7 +50,6 @@ class YalePanicButton(YaleAlarmEntity, ButtonEntity): """Initialize the plug switch.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.entry.data[CONF_NAME]} {description.name}" self._attr_unique_id = f"yale_smart_alarm-{description.key}" async def async_press(self) -> None: diff --git a/homeassistant/components/yale_smart_alarm/entity.py b/homeassistant/components/yale_smart_alarm/entity.py index b9a832f88e8..86b5839b51f 100644 --- a/homeassistant/components/yale_smart_alarm/entity.py +++ b/homeassistant/components/yale_smart_alarm/entity.py @@ -12,13 +12,14 @@ from .coordinator import YaleDataUpdateCoordinator class YaleEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): """Base implementation for Yale device.""" + _attr_has_entity_name = True + def __init__(self, coordinator: YaleDataUpdateCoordinator, data: dict) -> None: """Initialize an Yale device.""" super().__init__(coordinator) - self._attr_name: str = data["name"] self._attr_unique_id: str = data["address"] self._attr_device_info: DeviceInfo = DeviceInfo( - name=self._attr_name, + name=data["name"], manufacturer=MANUFACTURER, model=MODEL, identifiers={(DOMAIN, data["address"])}, @@ -29,6 +30,8 @@ class YaleEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): class YaleAlarmEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): """Base implementation for Yale Alarm device.""" + _attr_has_entity_name = True + def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: """Initialize an Yale device.""" super().__init__(coordinator) diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index a97a98a2afb..8f9ed6c9ce1 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -46,6 +46,7 @@ class YaleDoorlock(YaleEntity, LockEntity): """Initialize the Yale Lock Device.""" super().__init__(coordinator, data) self._attr_code_format = f"^\\d{code_format}$" + self.lock_name = data["name"] async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" @@ -65,7 +66,7 @@ class YaleDoorlock(YaleEntity, LockEntity): try: get_lock = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.get, self._attr_name + self.coordinator.yale.lock_api.get, self.lock_name ) if command == "locked": lock_state = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/yale_smart_alarm/translations/pt.json b/homeassistant/components/yale_smart_alarm/translations/pt.json new file mode 100644 index 00000000000..0b1a6c82cf3 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "name": "Nome", + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index a3362e0558a..b8117df056a 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.musiccast.device.enable_polling() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 4fd742e24f5..c07852629a9 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -216,7 +216,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (asyncio.TimeoutError, OSError, BulbException) as ex: raise ConfigEntryNotReady from ex - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Wait to install the reload listener until everything was successfully initialized entry.async_on_unload(entry.add_update_listener(_async_update_listener)) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 92068d1e26e..3257d64d265 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -27,6 +27,7 @@ SCAN_INTERVAL = timedelta(minutes=5) PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.COVER, Platform.LOCK, Platform.SENSOR, Platform.SIREN, @@ -108,7 +109,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_coordinator.data = {} device_coordinators[device.device_id] = device_coordinator hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATORS] = device_coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index b296e01fa56..6e29bccf437 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -47,6 +47,13 @@ SENSOR_DEVICE_TYPE = [ ] +def is_door_sensor(device: YoLinkDevice) -> bool: + """Check Door Sensor type.""" + return device.device_type == ATTR_DEVICE_DOOR_SENSOR and ( + device.parent_id is None or device.parent_id == "null" + ) + + SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( YoLinkBinarySensorEntityDescription( key="door_state", @@ -54,7 +61,7 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.DOOR, name="State", value=lambda value: value == "open" if value is not None else None, - exists_fn=lambda device: device.device_type == ATTR_DEVICE_DOOR_SENSOR, + exists_fn=is_door_sensor, ), YoLinkBinarySensorEntityDescription( key="motion_state", diff --git a/homeassistant/components/yolink/cover.py b/homeassistant/components/yolink/cover.py new file mode 100644 index 00000000000..e7e32764ca8 --- /dev/null +++ b/homeassistant/components/yolink/cover.py @@ -0,0 +1,86 @@ +"""YoLink Garage Door.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import ( + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_COORDINATORS, ATTR_DEVICE_DOOR_SENSOR, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink garage door from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + entities = [ + YoLinkCoverEntity(config_entry, device_coordinator) + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type == ATTR_DEVICE_DOOR_SENSOR + and device_coordinator.device.parent_id is not None + and device_coordinator.device.parent_id != "null" + ] + async_add_entities(entities) + + +class YoLinkCoverEntity(YoLinkEntity, CoverEntity): + """YoLink Cover Entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + ) -> None: + """Init YoLink garage door entity.""" + super().__init__(config_entry, coordinator) + self._attr_unique_id = f"{coordinator.device.device_id}_door_state" + self._attr_name = f"{coordinator.device.device_name} (State)" + self._attr_device_class = CoverDeviceClass.GARAGE + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + + @callback + def update_entity_state(self, state: dict[str, Any]) -> None: + """Update HA Entity State.""" + self._attr_is_closed = state.get("state") == "closed" + self.async_write_ha_state() + + async def toggle_garage_state(self, state: str) -> None: + """Toggle Garage door state.""" + # make sure current state is correct + await self.coordinator.async_refresh() + if state == "open" and self.is_closed is False: + return + if state == "close" and self.is_closed is True: + return + # get paired controller + door_controller_coordinator = self.hass.data[DOMAIN][ + self.config_entry.entry_id + ][ATTR_COORDINATORS].get(self.coordinator.device.parent_id) + if door_controller_coordinator is None: + raise ValueError( + "This device has not been paired with a garage door controller" + ) + # call controller api open/close garage door + await door_controller_coordinator.device.call_device_http_api("toggle", None) + await self.coordinator.async_refresh() + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open garage door.""" + await self.toggle_garage_state("open") + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close garage door.""" + await self.toggle_garage_state("close") diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index d2a02a44c42..0db736938f7 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.8"], + "requirements": ["yolink-api==0.0.9"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 4679c3e670b..6a7c7ea4cff 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -52,6 +52,7 @@ class YoLinkSensorEntityDescription( SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, diff --git a/homeassistant/components/yolink/translations/ja.json b/homeassistant/components/yolink/translations/ja.json index 7d2545803bf..274296a1240 100644 --- a/homeassistant/components/yolink/translations/ja.json +++ b/homeassistant/components/yolink/translations/ja.json @@ -17,8 +17,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Yolink\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Yolink\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/yolink/translations/pt.json b/homeassistant/components/yolink/translations/pt.json new file mode 100644 index 00000000000..1ffa7cb5245 --- /dev/null +++ b/homeassistant/components/yolink/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py index 3339cdccd36..0026d2ec484 100644 --- a/homeassistant/components/youless/__init__.py +++ b/homeassistant/components/youless/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/youless/translations/pt.json b/homeassistant/components/youless/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/youless/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 29afd5fc236..1bfa44f3894 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -424,11 +424,16 @@ class ZeroconfDiscovery: # Since we prefer local control, if the integration that is being discovered # is cloud AND the homekit device is UNPAIRED we still want to discovery it. # + # Additionally if the integration is polling, HKC offers a local push + # experience for the user to control the device so we want to offer that + # as well. + # # As soon as the device becomes paired, the config flow will be dismissed # in the event the user does not want to pair with Home Assistant. # - if not integration.iot_class or not integration.iot_class.startswith( - "cloud" + if not integration.iot_class or ( + not integration.iot_class.startswith("cloud") + and "polling" not in integration.iot_class ): return diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py index e8cc6962a0b..43a768c3844 100644 --- a/homeassistant/components/zerproc/__init__.py +++ b/homeassistant/components/zerproc/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if DATA_ADDRESSES not in hass.data[DOMAIN]: hass.data[DOMAIN][DATA_ADDRESSES] = set() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/zerproc/translations/ja.json b/homeassistant/components/zerproc/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/zerproc/translations/ja.json +++ b/homeassistant/components/zerproc/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 0e11d992a25..0a7d43120f7 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,6 +1,7 @@ """Support for Zigbee Home Automation devices.""" import asyncio import logging +import os import voluptuous as vol from zhaquirks import setup as setup_quirks @@ -12,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType from . import api @@ -29,11 +31,11 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_GATEWAY, - DATA_ZHA_PLATFORM_LOADED, DATA_ZHA_SHUTDOWN_TASK, DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, + ZHA_DEVICES_LOADED_EVENT, RadioType, ) from .core.discovery import GROUP_PROBE @@ -74,7 +76,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up ZHA from config.""" - hass.data[DATA_ZHA] = {} + hass.data[DATA_ZHA] = {ZHA_DEVICES_LOADED_EVENT: asyncio.Event()} if DOMAIN in config: conf = config[DOMAIN] @@ -98,21 +100,23 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if config.get(CONF_ENABLE_QUIRKS, True): setup_quirks(config) + # temporary code to remove the zha storage file from disk. this will be removed in 2022.10.0 + storage_path = hass.config.path(STORAGE_DIR, "zha.storage") + if os.path.isfile(storage_path): + _LOGGER.debug("removing ZHA storage file") + await hass.async_add_executor_job(os.remove, storage_path) + else: + _LOGGER.debug("ZHA storage file does not exist or was already removed") + zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() - - zha_data[DATA_ZHA_PLATFORM_LOADED] = [] - for platform in PLATFORMS: - coro = hass.config_entries.async_forward_entry_setup(config_entry, platform) - zha_data[DATA_ZHA_PLATFORM_LOADED].append(hass.async_create_task(coro)) + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].set() device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={ - (dr.CONNECTION_ZIGBEE, str(zha_gateway.application_controller.ieee)) - }, - identifiers={(DOMAIN, str(zha_gateway.application_controller.ieee))}, + connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.coordinator_ieee))}, + identifiers={(DOMAIN, str(zha_gateway.coordinator_ieee))}, name="Zigbee Coordinator", manufacturer="ZHA", model=zha_gateway.radio_description, @@ -124,12 +128,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Handle shutdown tasks.""" zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY] await zha_gateway.shutdown() - await zha_gateway.async_update_device_storage() zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown ) - asyncio.create_task(async_load_entities(hass)) + + await zha_gateway.async_initialize_devices_and_entities() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) return True @@ -137,7 +143,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload ZHA config entry.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] await zha_gateway.shutdown() - await zha_gateway.async_update_device_storage() + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].clear() GROUP_PROBE.cleanup() api.async_unload_api(hass) @@ -155,18 +161,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def async_load_entities(hass: HomeAssistant) -> None: - """Load entities after integration was setup.""" - zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - await zha_gateway.async_initialize_devices_and_entities() - to_setup = hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED] - results = await asyncio.gather(*to_setup, return_exceptions=True) - for res in results: - if isinstance(res, Exception): - _LOGGER.warning("Couldn't setup zha platform: %s", res) - async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) - - async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index cc4dd45689e..89d360577d4 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1091,10 +1091,7 @@ def async_load_api(hass: HomeAssistant) -> None: zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = service.data[ATTR_IEEE] zha_device: ZHADevice | None = zha_gateway.get_device(ieee) - if zha_device is not None and ( - zha_device.is_coordinator - and zha_device.ieee == zha_gateway.application_controller.ieee - ): + if zha_device is not None and zha_device.is_active_coordinator: _LOGGER.info("Removing the coordinator (%s) is not allowed", ieee) return _LOGGER.info("Removing node %s", ieee) diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 0f98bfaad51..fcc040cbde2 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -4,7 +4,7 @@ from __future__ import annotations import abc import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.foundation import Status @@ -27,6 +27,8 @@ if TYPE_CHECKING: from .core.device import ZHADevice +_ZHAIdentifyButtonSelfT = TypeVar("_ZHAIdentifyButtonSelfT", bound="ZHAIdentifyButton") + MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.BUTTON) CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.BUTTON @@ -66,7 +68,7 @@ class ZHAButton(ZhaEntity, ButtonEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this button.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -89,12 +91,12 @@ class ZHAIdentifyButton(ZHAButton): @classmethod def create_entity( - cls, + cls: type[_ZHAIdentifyButtonSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZHAIdentifyButtonSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -126,7 +128,7 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this button.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 9042856b456..849cdc29e47 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -36,8 +36,8 @@ if TYPE_CHECKING: from ...entity import ZhaEntity from ..device import ZHADevice -_ChannelsT = TypeVar("_ChannelsT", bound="Channels") -_ChannelPoolT = TypeVar("_ChannelPoolT", bound="ChannelPool") +_ChannelsSelfT = TypeVar("_ChannelsSelfT", bound="Channels") +_ChannelPoolSelfT = TypeVar("_ChannelPoolSelfT", bound="ChannelPool") _ChannelsDictType = dict[str, base.ZigbeeChannel] @@ -104,7 +104,7 @@ class Channels: } @classmethod - def new(cls: type[_ChannelsT], zha_device: ZHADevice) -> _ChannelsT: + def new(cls: type[_ChannelsSelfT], zha_device: ZHADevice) -> _ChannelsSelfT: """Create new instance.""" channels = cls(zha_device) for ep_id in sorted(zha_device.device.endpoints): @@ -272,7 +272,9 @@ class ChannelPool: ) @classmethod - def new(cls: type[_ChannelPoolT], channels: Channels, ep_id: int) -> _ChannelPoolT: + def new( + cls: type[_ChannelPoolSelfT], channels: Channels, ep_id: int + ) -> _ChannelPoolSelfT: """Create new channels for an endpoint.""" pool = cls(channels, ep_id) pool.add_all_channels() diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index b2870d84e15..d310157327b 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -365,7 +365,7 @@ class OnOffChannel(ZigbeeChannel): should_accept = args[0] on_time = args[1] # 0 is always accept 1 is only accept when already on - if should_accept == 0 or (should_accept == 1 and self._state): + if should_accept == 0 or (should_accept == 1 and bool(self.on_off)): if self._off_listener is not None: self._off_listener() self._off_listener = None diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 99e6101b0bd..36bb0beb17d 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -31,6 +31,9 @@ class ColorChannel(ZigbeeChannel): REPORT_CONFIG = ( AttrReportConfig(attr="current_x", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="current_y", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="current_hue", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="enhanced_current_hue", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="current_saturation", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="color_temperature", config=REPORT_CONFIG_DEFAULT), ) MAX_MIREDS: int = 500 @@ -52,6 +55,14 @@ class ColorChannel(ZigbeeChannel): return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP return self.CAPABILITIES_COLOR_XY + @property + def zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities: + """Return ZCL color capabilities of the light.""" + color_capabilities = self.cluster.get("color_capabilities") + if color_capabilities is None: + return lighting.Color.ColorCapabilities(self.CAPABILITIES_COLOR_XY) + return lighting.Color.ColorCapabilities(color_capabilities) + @property def color_mode(self) -> int | None: """Return cached value of the color_mode attribute.""" @@ -77,6 +88,21 @@ class ColorChannel(ZigbeeChannel): """Return cached value of the current_y attribute.""" return self.cluster.get("current_y") + @property + def current_hue(self) -> int | None: + """Return cached value of the current_hue attribute.""" + return self.cluster.get("current_hue") + + @property + def enhanced_current_hue(self) -> int | None: + """Return cached value of the enhanced_current_hue attribute.""" + return self.cluster.get("enhanced_current_hue") + + @property + def current_saturation(self) -> int | None: + """Return cached value of the current_saturation attribute.""" + return self.cluster.get("current_saturation") + @property def min_mireds(self) -> int: """Return the coldest color_temp that this channel supports.""" @@ -86,3 +112,48 @@ class ColorChannel(ZigbeeChannel): def max_mireds(self) -> int: """Return the warmest color_temp that this channel supports.""" return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) + + @property + def hs_supported(self) -> bool: + """Return True if the channel supports hue and saturation.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Hue_and_saturation + in self.zcl_color_capabilities + ) + + @property + def enhanced_hue_supported(self) -> bool: + """Return True if the channel supports enhanced hue and saturation.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Enhanced_hue + in self.zcl_color_capabilities + ) + + @property + def xy_supported(self) -> bool: + """Return True if the channel supports xy.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.XY_attributes + in self.zcl_color_capabilities + ) + + @property + def color_temp_supported(self) -> bool: + """Return True if the channel supports color temperature.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Color_temperature + in self.zcl_color_capabilities + ) + + @property + def color_loop_supported(self) -> bool: + """Return True if the channel supports color loop.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Color_loop + in self.zcl_color_capabilities + ) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 943d13a57d6..b9f0ec1aaca 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -75,7 +75,7 @@ class OppleRemote(ZigbeeChannel): "trigger_indicator": True, } elif self.cluster.endpoint.model == "lumi.motion.ac01": - self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name + self.ZCL_INIT_ATTRS = { "presence": True, "monitoring_mode": True, "motion_sensitivity": True, diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c2d9e926453..dfa5f608cfe 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -17,6 +17,7 @@ import zigpy_znp.zigbee.application from homeassistant.const import Platform import homeassistant.helpers.config_validation as cv +ATTR_ACTIVE_COORDINATOR = "active_coordinator" ATTR_ARGS = "args" ATTR_ATTRIBUTE = "attribute" ATTR_ATTRIBUTE_ID = "attribute_id" @@ -127,6 +128,9 @@ CONF_CUSTOM_QUIRKS_PATH = "custom_quirks_path" CONF_DATABASE = "database_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" +CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" +CONF_ENABLE_LIGHT_TRANSITIONING_FLAG = "light_transitioning_flag" +CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_FLOWCONTROL = "flow_control" @@ -142,6 +146,9 @@ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, + vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, + vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean, + vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Optional( CONF_CONSIDER_UNAVAILABLE_MAINS, @@ -170,7 +177,6 @@ DATA_ZHA_CONFIG = "config" DATA_ZHA_BRIDGE_ID = "zha_bridge_id" DATA_ZHA_CORE_EVENTS = "zha_core_events" DATA_ZHA_GATEWAY = "zha_gateway" -DATA_ZHA_PLATFORM_LOADED = "platform_loaded" DATA_ZHA_SHUTDOWN_TASK = "zha_shutdown_task" DEBUG_COMP_BELLOWS = "bellows" @@ -388,6 +394,7 @@ ZHA_GW_MSG_GROUP_REMOVED = "group_removed" ZHA_GW_MSG_LOG_ENTRY = "log_entry" ZHA_GW_MSG_LOG_OUTPUT = "log_output" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" +ZHA_DEVICES_LOADED_EVENT = "zha_devices_loaded_event" EFFECT_BLINK = 0x00 EFFECT_BREATHE = 0x01 diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e83c0afbceb..150241a091b 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -9,7 +9,7 @@ from functools import cached_property import logging import random import time -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from zigpy import types import zigpy.device @@ -30,6 +30,7 @@ from homeassistant.helpers.event import async_track_time_interval from . import channels from .const import ( + ATTR_ACTIVE_COORDINATOR, ATTR_ARGS, ATTR_ATTRIBUTE, ATTR_AVAILABLE, @@ -85,6 +86,8 @@ _LOGGER = logging.getLogger(__name__) _UPDATE_ALIVE_INTERVAL = (60, 90) _CHECKIN_GRACE_PERIODS = 2 +_ZHADeviceSelfT = TypeVar("_ZHADeviceSelfT", bound="ZHADevice") + class DeviceStatus(Enum): """Status of a device.""" @@ -252,12 +255,20 @@ class ZHADevice(LogMixin): @property def is_coordinator(self) -> bool | None: - """Return true if this device represents the coordinator.""" + """Return true if this device represents a coordinator.""" if self._zigpy_device.node_desc is None: return None return self._zigpy_device.node_desc.is_coordinator + @property + def is_active_coordinator(self) -> bool: + """Return true if this device is the active coordinator.""" + if not self.is_coordinator: + return False + + return self.ieee == self.gateway.coordinator_ieee + @property def is_end_device(self) -> bool | None: """Return true if this device is an end device.""" @@ -331,12 +342,12 @@ class ZHADevice(LogMixin): @classmethod def new( - cls, + cls: type[_ZHADeviceSelfT], hass: HomeAssistant, zigpy_dev: zigpy.device.Device, gateway: ZHAGateway, restored: bool = False, - ): + ) -> _ZHADeviceSelfT: """Create new device.""" zha_dev = cls(hass, zigpy_dev, gateway) zha_dev.channels = channels.Channels.new(zha_dev) @@ -470,8 +481,6 @@ class ZHADevice(LogMixin): self.debug("started configuration") await self._channels.async_configure() self.debug("completed configuration") - entry = self.gateway.zha_storage.async_create_or_update_device(self) - self.debug("stored in registry: %s", entry) if ( should_identify @@ -496,17 +505,12 @@ class ZHADevice(LogMixin): for unsubscribe in self.unsubs: unsubscribe() - @callback - def async_update_last_seen(self, last_seen: float | None) -> None: - """Set last seen on the zigpy device.""" - if self._zigpy_device.last_seen is None and last_seen is not None: - self._zigpy_device.last_seen = last_seen - @property def zha_device_info(self) -> dict[str, Any]: """Get ZHA device information.""" device_info: dict[str, Any] = {} device_info.update(self.device_info) + device_info[ATTR_ACTIVE_COORDINATOR] = self.is_active_coordinator device_info["entities"] = [ { "entity_id": entity_ref.reference_id, diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 099abfe5e88..14fbf2cf701 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -8,6 +8,7 @@ from datetime import timedelta from enum import Enum import itertools import logging +import re import time import traceback from typing import TYPE_CHECKING, Any, NamedTuple, Union @@ -20,6 +21,7 @@ import zigpy.endpoint import zigpy.group from zigpy.types.named import EUI64 +from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -27,7 +29,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from . import discovery @@ -81,14 +82,12 @@ from .const import ( from .device import DeviceStatus, ZHADevice from .group import GroupMember, ZHAGroup from .registries import GROUP_ENTITY_DOMAINS -from .store import async_get_registry if TYPE_CHECKING: from logging import Filter, LogRecord from ..entity import ZhaEntity from .channels.base import ZigbeeChannel - from .store import ZhaStorage _LogFilterType = Union[Filter, Callable[[LogRecord], int]] @@ -118,7 +117,6 @@ class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" # -- Set in async_initialize -- - zha_storage: ZhaStorage ha_device_registry: dr.DeviceRegistry ha_entity_registry: er.EntityRegistry application_controller: ControllerApplication @@ -144,13 +142,13 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + self.initialized: bool = False async def async_initialize(self) -> None: """Initialize controller and connect radio.""" discovery.PROBE.initialize(self._hass) discovery.GROUP_PROBE.initialize(self._hass) - self.zha_storage = await async_get_registry(self._hass) self.ha_device_registry = dr.async_get(self._hass) self.ha_entity_registry = er.async_get(self._hass) @@ -183,23 +181,21 @@ class ZHAGateway: self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self - self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str( - self.application_controller.ieee - ) + self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) self.async_load_devices() self.async_load_groups() + self.initialized = True @callback def async_load_devices(self) -> None: """Restore ZHA devices from zigpy application state.""" for zigpy_device in self.application_controller.devices.values(): zha_device = self._async_get_or_create_device(zigpy_device, restored=True) - if zha_device.ieee == self.application_controller.ieee: + if zha_device.ieee == self.coordinator_ieee: self.coordinator_zha_device = zha_device - zha_dev_entry = self.zha_storage.devices.get(str(zigpy_device.ieee)) delta_msg = "not known" - if zha_dev_entry and zha_dev_entry.last_seen is not None: - delta = round(time.time() - zha_dev_entry.last_seen) + if zha_device.last_seen is not None: + delta = round(time.time() - zha_device.last_seen) zha_device.available = delta < zha_device.consider_unavailable_time delta_msg = f"{str(timedelta(seconds=delta))} ago" _LOGGER.debug( @@ -210,13 +206,6 @@ class ZHAGateway: delta_msg, zha_device.consider_unavailable_time, ) - # update the last seen time for devices every 10 minutes to avoid thrashing - # writes and shutdown issues where storage isn't updated - self._unsubs.append( - async_track_time_interval( - self._hass, self.async_update_device_storage, timedelta(minutes=10) - ) - ) @callback def async_load_groups(self) -> None: @@ -230,7 +219,7 @@ class ZHAGateway: async def async_initialize_devices_and_entities(self) -> None: """Initialize devices and load entities.""" - _LOGGER.debug("Loading all devices") + _LOGGER.debug("Initializing all devices from Zigpy cache") await asyncio.gather( *(dev.async_initialize(from_cache=True) for dev in self.devices.values()) ) @@ -448,6 +437,11 @@ class ZHAGateway: ) self.ha_entity_registry.async_remove(entry.entity_id) + @property + def coordinator_ieee(self) -> EUI64: + """Return the active coordinator's IEEE address.""" + return self.application_controller.state.node_info.ieee + @property def devices(self) -> dict[EUI64, ZHADevice]: """Return devices.""" @@ -526,8 +520,6 @@ class ZHAGateway: model=zha_device.model, ) zha_device.set_device_id(device_registry_device.id) - entry = self.zha_storage.async_get_or_create_device(zha_device) - zha_device.async_update_last_seen(entry.last_seen) return zha_device @callback @@ -550,17 +542,9 @@ class ZHAGateway: if device.status is DeviceStatus.INITIALIZED: device.update_available(available) - async def async_update_device_storage(self, *_: Any) -> None: - """Update the devices in the store.""" - for device in self.devices.values(): - self.zha_storage.async_update_device(device) - async def async_device_initialized(self, device: zigpy.device.Device) -> None: """Handle device joined and basic information discovered (async).""" zha_device = self._async_get_or_create_device(device) - # This is an active device so set a last seen if it is none - if zha_device.last_seen is None: - zha_device.async_update_last_seen(time.time()) _LOGGER.debug( "device - %s:%s entering async_device_initialized - is_new_join: %s", device.nwk, @@ -752,7 +736,15 @@ class LogRelayHandler(logging.Handler): if record.levelno >= logging.WARN and not record.exc_info: stack = [f for f, _, _, _ in traceback.extract_stack()] - entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass)) + hass_path: str = HOMEASSISTANT_PATH[0] + config_dir = self.hass.config.config_dir + assert config_dir is not None + paths_re = re.compile( + r"(?:{})/(.*)".format( + "|".join([re.escape(x) for x in (hass_path, config_dir)]) + ) + ) + entry = LogEntry(record, stack, _figure_out_source(record, stack, paths_re)) async_dispatcher_send( self.hass, ZHA_GW_MSG, diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b60f61b1e8e..7fd789ac3f5 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -26,6 +26,7 @@ import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback +from homeassistant.exceptions import IntegrationError from homeassistant.helpers import device_registry as dr from .const import ( @@ -42,6 +43,7 @@ if TYPE_CHECKING: from .gateway import ZHAGateway _T = TypeVar("_T") +_LOGGER = logging.getLogger(__name__) @dataclass @@ -170,10 +172,22 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) if not registry_device: + _LOGGER.error("Device id `%s` not found in registry", device_id) raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = zigpy.types.EUI64.convert(ieee_address) + if not zha_gateway.initialized: + _LOGGER.error("Attempting to get a ZHA device when ZHA is not initialized") + raise IntegrationError("ZHA is not initialized yet") + try: + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = zigpy.types.EUI64.convert(ieee_address) + except (IndexError, ValueError) as ex: + _LOGGER.error( + "Unable to determine device IEEE for device with device id `%s`", device_id + ) + raise KeyError( + f"Unable to determine device IEEE for device with device id `{device_id}`." + ) from ex return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py deleted file mode 100644 index e58dcd46dba..00000000000 --- a/homeassistant/components/zha/core/store.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Data storage helper for ZHA.""" -from __future__ import annotations - -from collections import OrderedDict -from collections.abc import MutableMapping -import datetime -import time -from typing import TYPE_CHECKING, Any, cast - -import attr - -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.storage import Store -from homeassistant.loader import bind_hass - -if TYPE_CHECKING: - from .device import ZHADevice - -DATA_REGISTRY = "zha_storage" - -STORAGE_KEY = "zha.storage" -STORAGE_VERSION = 1 -SAVE_DELAY = 10 -TOMBSTONE_LIFETIME = datetime.timedelta(days=60).total_seconds() - - -@attr.s(slots=True, frozen=True) -class ZhaDeviceEntry: - """Zha Device storage Entry.""" - - name: str | None = attr.ib(default=None) - ieee: str | None = attr.ib(default=None) - last_seen: float | None = attr.ib(default=None) - - -class ZhaStorage: - """Class to hold a registry of zha devices.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the zha device storage.""" - self.hass: HomeAssistant = hass - self.devices: MutableMapping[str, ZhaDeviceEntry] = {} - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - - @callback - def async_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create a new ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - device_entry: ZhaDeviceEntry = ZhaDeviceEntry( - name=device.name, ieee=ieee_str, last_seen=device.last_seen - ) - self.devices[ieee_str] = device_entry - self.async_schedule_save() - return device_entry - - @callback - def async_get_or_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create a new ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - if ieee_str in self.devices: - return self.devices[ieee_str] - return self.async_create_device(device) - - @callback - def async_create_or_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create or update a ZhaDeviceEntry.""" - if str(device.ieee) in self.devices: - return self.async_update_device(device) - return self.async_create_device(device) - - @callback - def async_delete_device(self, device: ZHADevice) -> None: - """Delete ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - if ieee_str in self.devices: - del self.devices[ieee_str] - self.async_schedule_save() - - @callback - def async_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Update name of ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - old = self.devices[ieee_str] - - if device.last_seen is None: - return old - - changes = {} - changes["last_seen"] = device.last_seen - - new = self.devices[ieee_str] = attr.evolve(old, **changes) - self.async_schedule_save() - return new - - async def async_load(self) -> None: - """Load the registry of zha device entries.""" - data = cast(dict[str, Any], await self._store.async_load()) - - devices: OrderedDict[str, ZhaDeviceEntry] = OrderedDict() - - if data is not None: - for device in data["devices"]: - devices[device["ieee"]] = ZhaDeviceEntry( - name=device["name"], - ieee=device["ieee"], - last_seen=device.get("last_seen"), - ) - - self.devices = devices - - @callback - def async_schedule_save(self) -> None: - """Schedule saving the registry of zha devices.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - - async def async_save(self) -> None: - """Save the registry of zha devices.""" - await self._store.async_save(self._data_to_save()) - - @callback - def _data_to_save(self) -> dict: - """Return data for the registry of zha devices to store in a file.""" - data = {} - - data["devices"] = [ - {"name": entry.name, "ieee": entry.ieee, "last_seen": entry.last_seen} - for entry in self.devices.values() - if entry.last_seen and (time.time() - entry.last_seen) < TOMBSTONE_LIFETIME - ] - - return data - - -@bind_hass -async def async_get_registry(hass: HomeAssistant) -> ZhaStorage: - """Return zha device storage instance.""" - if (task := hass.data.get(DATA_REGISTRY)) is None: - - async def _load_reg() -> ZhaStorage: - registry = ZhaStorage(hass) - await registry.async_load() - return registry - - task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg()) - - return cast(ZhaStorage, await task) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 44682aaa559..4ad8eccea1d 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -13,11 +13,11 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, IntegrationError from homeassistant.helpers.typing import ConfigType -from . import DOMAIN -from .core.const import ZHA_EVENT +from . import DOMAIN as ZHA_DOMAIN +from .core.const import DATA_ZHA, ZHA_DEVICES_LOADED_EVENT, ZHA_EVENT from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" @@ -35,11 +35,12 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) - if "zha" in hass.config.components: + if ZHA_DOMAIN in hass.config.components: + await hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].wait() trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError) as err: + except (KeyError, AttributeError, IntegrationError) as err: raise InvalidDeviceAutomationConfig from err if ( zha_device.device_automation_triggers is None @@ -100,7 +101,7 @@ async def async_get_triggers( triggers.append( { CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, + CONF_DOMAIN: ZHA_DOMAIN, CONF_PLATFORM: DEVICE, CONF_TYPE: trigger, CONF_SUBTYPE: subtype, diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index f70948eb04a..2f609555c79 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Callable import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from homeassistant.const import ATTR_NAME from homeassistant.core import CALLBACK_TYPE, Event, callback @@ -35,6 +35,9 @@ if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel from .core.device import ZHADevice +_ZhaEntitySelfT = TypeVar("_ZhaEntitySelfT", bound="ZhaEntity") +_ZhaGroupEntitySelfT = TypeVar("_ZhaGroupEntitySelfT", bound="ZhaGroupEntity") + _LOGGER = logging.getLogger(__name__) ENTITY_SUFFIX = "entity_suffix" @@ -45,6 +48,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): """A base class for ZHA entities.""" unique_id_suffix: str | None = None + _attr_has_entity_name = True def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None: """Init ZHA entity.""" @@ -154,7 +158,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): class ZhaEntity(BaseZhaEntity, RestoreEntity): """A base class for non group ZHA entities.""" - def __init_subclass__(cls, id_suffix: str | None = None, **kwargs) -> None: + def __init_subclass__(cls, id_suffix: str | None = None, **kwargs: Any) -> None: """Initialize subclass. :param id_suffix: suffix to add to the unique_id of the entity. Used for multi @@ -173,23 +177,25 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): ) -> None: """Init ZHA entity.""" super().__init__(unique_id, zha_device, **kwargs) - ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) - ch_names = ", ".join(sorted(ch.name for ch in channels)) - self._name: str = f"{zha_device.name} {ieeetail} {ch_names}" - if self.unique_id_suffix: - self._name += f" {self.unique_id_suffix}" + self._name: str = ( + self.__class__.__name__.lower() + .replace("zha", "") + .replace("entity", "") + .replace("sensor", "") + .capitalize() + ) self.cluster_channels: dict[str, ZigbeeChannel] = {} for channel in channels: self.cluster_channels[channel.name] = channel @classmethod def create_entity( - cls, + cls: type[_ZhaEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZhaEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -254,13 +260,20 @@ class ZhaGroupEntity(BaseZhaEntity): """A base class for ZHA group entities.""" def __init__( - self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs + self, + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, ) -> None: """Initialize a light group.""" super().__init__(unique_id, zha_device, **kwargs) self._available = False self._group = zha_device.gateway.groups.get(group_id) - self._name = f"{self._group.name}_zha_group_0x{group_id:04x}" + self._name = ( + f"{self._group.name}_zha_group_0x{group_id:04x}".lower().capitalize() + ) self._group_id: int = group_id self._entity_ids: list[str] = entity_ids self._async_unsub_state_changed: CALLBACK_TYPE | None = None @@ -274,8 +287,13 @@ class ZhaGroupEntity(BaseZhaEntity): @classmethod def create_entity( - cls, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs - ) -> ZhaGroupEntity | None: + cls: type[_ZhaGroupEntitySelfT], + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, + ) -> _ZhaGroupEntitySelfT | None: """Group Entity Factory. Return entity if it is a supported configuration, otherwise return None diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index cc8dc475c43..9fc089e2241 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,7 +1,9 @@ """Lights on Zigbee Home Automation networks.""" from __future__ import annotations +import asyncio from collections import Counter +from collections.abc import Callable from datetime import timedelta import functools import itertools @@ -15,15 +17,6 @@ from zigpy.zcl.foundation import Status from homeassistant.components import light from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_MODE, - ATTR_COLOR_TEMP, - ATTR_EFFECT, - ATTR_EFFECT_LIST, - ATTR_HS_COLOR, - ATTR_MAX_MIREDS, - ATTR_MIN_MIREDS, - ATTR_SUPPORTED_COLOR_MODES, ColorMode, brightness_supported, filter_supported_color_modes, @@ -35,22 +28,24 @@ from homeassistant.const import ( STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_time_interval -import homeassistant.util.color as color_util +from homeassistant.helpers.event import async_call_later, async_track_time_interval from .core import discovery, helpers from .core.const import ( CHANNEL_COLOR, CHANNEL_LEVEL, CHANNEL_ON_OFF, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, CONF_DEFAULT_LIGHT_TRANSITION, + CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, DATA_ZHA, EFFECT_BLINK, EFFECT_BREATHE, @@ -69,10 +64,10 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -CAPABILITIES_COLOR_LOOP = 0x4 -CAPABILITIES_COLOR_XY = 0x08 -CAPABILITIES_COLOR_TEMP = 0x10 - +DEFAULT_ON_OFF_TRANSITION = 1 # most bulbs default to a 1-second turn on/off transition +DEFAULT_EXTRA_TRANSITION_DELAY_SHORT = 0.25 +DEFAULT_EXTRA_TRANSITION_DELAY_LONG = 2.0 +DEFAULT_LONG_TRANSITION_TIME = 10 DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 @@ -87,8 +82,11 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" +SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start" +SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished" +DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"} -COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.HS} +COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} SUPPORT_GROUP_LIGHT = ( light.LightEntityFeature.EFFECT | light.LightEntityFeature.FLASH @@ -122,25 +120,25 @@ class BaseLight(LogMixin, light.LightEntity): def __init__(self, *args, **kwargs): """Initialize the light.""" + self._zha_device: ZHADevice = None super().__init__(*args, **kwargs) - self._available: bool = False - self._brightness: int | None = None + self._attr_min_mireds: int | None = 153 + self._attr_max_mireds: int | None = 500 + self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes + self._attr_supported_features: int = 0 + self._attr_state: bool | None self._off_with_transition: bool = False self._off_brightness: int | None = None - self._hs_color: tuple[float, float] | None = None - self._color_temp: int | None = None - self._min_mireds: int | None = 153 - self._max_mireds: int | None = 500 - self._effect_list: list[str] | None = None - self._effect: str | None = None - self._supported_features: int = 0 - self._state: bool = False + self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME + self._zha_config_enhanced_light_transition: bool = False + self._zha_config_enable_light_transitioning_flag: bool = True + self._zha_config_always_prefer_xy_color_mode: bool = True self._on_off_channel = None self._level_channel = None self._color_channel = None self._identify_channel = None - self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME - self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes + self._transitioning: bool = False + self._transition_listener: Callable[[], None] | None = None @property def extra_state_attributes(self) -> dict[str, Any]: @@ -154,24 +152,9 @@ class BaseLight(LogMixin, light.LightEntity): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state is None: + if self._attr_state is None: return False - return self._state - - @property - def brightness(self): - """Return the brightness of this light.""" - return self._brightness - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._min_mireds - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._max_mireds + return self._attr_state @callback def set_level(self, value): @@ -181,35 +164,16 @@ class BaseLight(LogMixin, light.LightEntity): on at `on_level` Zigbee attribute value, regardless of the last set level """ + if self._transitioning: + self.debug( + "received level %s while transitioning - skipping update", + value, + ) + return value = max(0, min(254, value)) - self._brightness = value + self._attr_brightness = value self.async_write_ha_state() - @property - def hs_color(self): - """Return the hs color value [int, int].""" - return self._hs_color - - @property - def color_temp(self): - """Return the CT color value in mireds.""" - return self._color_temp - - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._effect_list - - @property - def effect(self): - """Return the current effect.""" - return self._effect - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) @@ -222,8 +186,39 @@ class BaseLight(LogMixin, light.LightEntity): effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) temperature = kwargs.get(light.ATTR_COLOR_TEMP) + xy_color = kwargs.get(light.ATTR_XY_COLOR) hs_color = kwargs.get(light.ATTR_HS_COLOR) + set_transition_flag = ( + brightness_supported(self._attr_supported_color_modes) + or temperature is not None + or xy_color is not None + or hs_color is not None + ) and self._zha_config_enable_light_transitioning_flag + transition_time = ( + ( + duration / 10 + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + if ( + (brightness is not None or transition is not None) + and brightness_supported(self._attr_supported_color_modes) + or (self._off_with_transition and self._off_brightness is not None) + or temperature is not None + or xy_color is not None + or hs_color is not None + ) + else DEFAULT_ON_OFF_TRANSITION + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + ) + if set_transition_flag + else 0 + ) + + # If we need to pause attribute report parsing, we'll do so here. + # After successful calls, we later start a timer to unset the flag after transition_time. + # On an error on the first move to level call, we unset the flag immediately if no previous timer is running. + # On an error on subsequent calls, we start the transition timer, as a brightness call might have come through. + if set_transition_flag: + self.async_transition_set_flag() + # If the light is currently off but a turn_on call with a color/temperature is sent, # the light needs to be turned on first at a low brightness level where the light is immediately transitioned # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness. @@ -233,21 +228,28 @@ class BaseLight(LogMixin, light.LightEntity): # move to level, on, color, move to level... We also will not set this if the bulb is already in the # desired color mode with the desired color or color temperature. new_color_provided_while_off = ( - not isinstance(self, LightGroup) + self._zha_config_enhanced_light_transition and not self._FORCE_ON - and not self._state + and not self._attr_state and ( ( temperature is not None and ( - self._color_temp != temperature + self._attr_color_temp != temperature or self._attr_color_mode != ColorMode.COLOR_TEMP ) ) + or ( + xy_color is not None + and ( + self._attr_xy_color != xy_color + or self._attr_color_mode != ColorMode.XY + ) + ) or ( hs_color is not None and ( - self.hs_color != hs_color + self._attr_hs_color != hs_color or self._attr_color_mode != ColorMode.HS ) ) @@ -265,7 +267,7 @@ class BaseLight(LogMixin, light.LightEntity): if brightness is not None: level = min(254, brightness) else: - level = self._brightness or 254 + level = self._attr_brightness or 254 t_log = {} @@ -277,10 +279,14 @@ class BaseLight(LogMixin, light.LightEntity): ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + # First 'move to level' call failed, so if the transitioning delay isn't running from a previous call, + # the flag can be unset immediately + if set_transition_flag and not self._transition_listener: + self.async_transition_complete() self.debug("turned on: %s", t_log) return # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call - self._state = True + self._attr_state = True if ( (brightness is not None or transition) @@ -292,11 +298,15 @@ class BaseLight(LogMixin, light.LightEntity): ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + # First 'move to level' call failed, so if the transitioning delay isn't running from a previous call, + # the flag can be unset immediately + if set_transition_flag and not self._transition_listener: + self.async_transition_complete() self.debug("turned on: %s", t_log) return - self._state = bool(level) + self._attr_state = bool(level) if level: - self._brightness = level + self._attr_brightness = level if ( brightness is None @@ -308,41 +318,25 @@ class BaseLight(LogMixin, light.LightEntity): result = await self._on_off_channel.on() t_log["on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + # 'On' call failed, but as brightness may still transition (for FORCE_ON lights), + # we start the timer to unset the flag after the transition_time if necessary. + self.async_transition_start_timer(transition_time) self.debug("turned on: %s", t_log) return - self._state = True + self._attr_state = True - if temperature is not None: - result = await self._color_channel.move_to_color_temp( - temperature, - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["move_to_color_temp"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - self.debug("turned on: %s", t_log) - return - self._attr_color_mode = ColorMode.COLOR_TEMP - self._color_temp = temperature - self._hs_color = None - - if hs_color is not None: - xy_color = color_util.color_hs_to_xy(*hs_color) - result = await self._color_channel.move_to_color( - int(xy_color[0] * 65535), - int(xy_color[1] * 65535), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["move_to_color"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - self.debug("turned on: %s", t_log) - return - self._attr_color_mode = ColorMode.HS - self._hs_color = hs_color - self._color_temp = None + if not await self.async_handle_color_commands( + temperature, + duration, + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + # Color calls failed, but as brightness may still transition, we start the timer to unset the flag + self.async_transition_start_timer(transition_time) + self.debug("turned on: %s", t_log) + return if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. @@ -351,9 +345,13 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._state = bool(level) + self._attr_state = bool(level) if level: - self._brightness = level + self._attr_brightness = level + + # Our light is guaranteed to have just started the transitioning process if necessary, + # so we start the delay for the transition (to stop parsing attribute reports after the completed transition). + self.async_transition_start_timer(transition_time) if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( @@ -366,9 +364,10 @@ class BaseLight(LogMixin, light.LightEntity): 0, # no hue ) t_log["color_loop_set"] = result - self._effect = light.EFFECT_COLORLOOP + self._attr_effect = light.EFFECT_COLORLOOP elif ( - self._effect == light.EFFECT_COLORLOOP and effect != light.EFFECT_COLORLOOP + self._attr_effect == light.EFFECT_COLORLOOP + and effect != light.EFFECT_COLORLOOP ): result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION, @@ -378,7 +377,7 @@ class BaseLight(LogMixin, light.LightEntity): 0x0, # update action only, action off, no dir, time, hue ) t_log["color_loop_set"] = result - self._effect = None + self._attr_effect = None if flash is not None: result = await self._identify_channel.trigger_effect( @@ -396,25 +395,159 @@ class BaseLight(LogMixin, light.LightEntity): transition = kwargs.get(light.ATTR_TRANSITION) supports_level = brightness_supported(self._attr_supported_color_modes) + transition_time = ( + transition or self._DEFAULT_MIN_TRANSITION_TIME + if transition is not None + else DEFAULT_ON_OFF_TRANSITION + ) + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + # Start pausing attribute report parsing + if self._zha_config_enable_light_transitioning_flag: + self.async_transition_set_flag() + # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( - 0, transition * 10 + 0, transition * 10 or self._DEFAULT_MIN_TRANSITION_TIME ) else: result = await self._on_off_channel.off() + + # Pause parsing attribute reports until transition is complete + if self._zha_config_enable_light_transitioning_flag: + self.async_transition_start_timer(transition_time) self.debug("turned off: %s", result) if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return - self._state = False + self._attr_state = False if supports_level: # store current brightness so that the next turn_on uses it. self._off_with_transition = transition is not None - self._off_brightness = self._brightness + self._off_brightness = self._attr_brightness self.async_write_ha_state() + async def async_handle_color_commands( + self, + temperature, + duration, + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + """Process ZCL color commands.""" + if temperature is not None: + result = await self._color_channel.move_to_color_temp( + temperature, + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_color_temp"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self._attr_color_mode = ColorMode.COLOR_TEMP + self._attr_color_temp = temperature + self._attr_xy_color = None + self._attr_hs_color = None + + if hs_color is not None: + if ( + not isinstance(self, LightGroup) + and self._color_channel.enhanced_hue_supported + ): + result = await self._color_channel.enhanced_move_to_hue_and_saturation( + int(hs_color[0] * 65535 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["enhanced_move_to_hue_and_saturation"] = result + else: + result = await self._color_channel.move_to_hue_and_saturation( + int(hs_color[0] * 254 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_hue_and_saturation"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = hs_color + self._attr_xy_color = None + self._attr_color_temp = None + xy_color = None # don't set xy_color if it is also present + + if xy_color is not None: + result = await self._color_channel.move_to_color( + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_color"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = xy_color + self._attr_color_temp = None + self._attr_hs_color = None + + return True + + @callback + def async_transition_set_flag(self) -> None: + """Set _transitioning to True.""" + self.debug("setting transitioning flag to True") + self._transitioning = True + if isinstance(self, LightGroup): + async_dispatcher_send( + self.hass, + SIGNAL_LIGHT_GROUP_TRANSITION_START, + {"entity_ids": self._entity_ids}, + ) + if self._transition_listener is not None: + self._transition_listener() + + @callback + def async_transition_start_timer(self, transition_time) -> None: + """Start a timer to unset _transitioning after transition_time if necessary.""" + if not transition_time: + return + # For longer transitions, we want to extend the timer a bit more + if transition_time >= DEFAULT_LONG_TRANSITION_TIME: + transition_time += DEFAULT_EXTRA_TRANSITION_DELAY_LONG + self.debug("starting transitioning timer for %s", transition_time) + self._transition_listener = async_call_later( + self._zha_device.hass, + transition_time, + self.async_transition_complete, + ) + + @callback + def async_transition_complete(self, _=None) -> None: + """Set _transitioning to False and write HA state.""" + self.debug("transition complete - future attribute reports will write HA state") + self._transitioning = False + if self._transition_listener: + self._transition_listener() + self._transition_listener = None + self.async_write_ha_state() + if isinstance(self, LightGroup): + async_dispatcher_send( + self.hass, + SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED, + {"entity_ids": self._entity_ids}, + ) + if self._debounced_member_refresh is not None: + self.debug("transition complete - refreshing group member states") + asyncio.create_task(self._debounced_member_refresh.async_call()) + @STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}) class Light(BaseLight, ZhaEntity): @@ -427,44 +560,69 @@ class Light(BaseLight, ZhaEntity): """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] - self._state = bool(self._on_off_channel.on_off) + self._attr_state = bool(self._on_off_channel.on_off) self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) self._identify_channel = self.zha_device.channels.identify_ch if self._color_channel: - self._min_mireds: int | None = self._color_channel.min_mireds - self._max_mireds: int | None = self._color_channel.max_mireds + self._attr_min_mireds: int = self._color_channel.min_mireds + self._attr_max_mireds: int = self._color_channel.max_mireds self._cancel_refresh_handle = None effect_list = [] + self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + True, + ) + self._attr_supported_color_modes = {ColorMode.ONOFF} if self._level_channel: self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) - self._supported_features |= light.LightEntityFeature.TRANSITION - self._brightness = self._level_channel.current_level + self._attr_supported_features |= light.LightEntityFeature.TRANSITION + self._attr_brightness = self._level_channel.current_level if self._color_channel: - color_capabilities = self._color_channel.color_capabilities - if color_capabilities & CAPABILITIES_COLOR_TEMP: + if self._color_channel.color_temp_supported: self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) - self._color_temp = self._color_channel.color_temperature + self._attr_color_temp = self._color_channel.color_temperature - if color_capabilities & CAPABILITIES_COLOR_XY: - self._attr_supported_color_modes.add(ColorMode.HS) + if self._color_channel.xy_supported and ( + self._zha_config_always_prefer_xy_color_mode + or not self._color_channel.hs_supported + ): + self._attr_supported_color_modes.add(ColorMode.XY) curr_x = self._color_channel.current_x curr_y = self._color_channel.current_y if curr_x is not None and curr_y is not None: - self._hs_color = color_util.color_xy_to_hs( - float(curr_x / 65535), float(curr_y / 65535) + self._attr_xy_color = (curr_x / 65535, curr_y / 65535) + else: + self._attr_xy_color = (0, 0) + + if ( + self._color_channel.hs_supported + and not self._zha_config_always_prefer_xy_color_mode + ): + self._attr_supported_color_modes.add(ColorMode.HS) + if self._color_channel.enhanced_hue_supported: + curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 + else: + curr_hue = self._color_channel.current_hue * 254 / 360 + curr_saturation = self._color_channel.current_saturation + if curr_hue is not None and curr_saturation is not None: + self._attr_hs_color = ( + int(curr_hue), + int(curr_saturation * 2.54), ) else: - self._hs_color = (0, 0) + self._attr_hs_color = (0, 0) - if color_capabilities & CAPABILITIES_COLOR_LOOP: - self._supported_features |= light.LightEntityFeature.EFFECT + if self._color_channel.color_loop_supported: + self._attr_supported_features |= light.LightEntityFeature.EFFECT effect_list.append(light.EFFECT_COLORLOOP) if self._color_channel.color_loop_active == 1: - self._effect = light.EFFECT_COLORLOOP + self._attr_effect = light.EFFECT_COLORLOOP self._attr_supported_color_modes = filter_supported_color_modes( self._attr_supported_color_modes ) @@ -475,13 +633,13 @@ class Light(BaseLight, ZhaEntity): if self._color_channel.color_mode == Color.ColorMode.Color_temperature: self._attr_color_mode = ColorMode.COLOR_TEMP else: - self._attr_color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.XY if self._identify_channel: - self._supported_features |= light.LightEntityFeature.FLASH + self._attr_supported_features |= light.LightEntityFeature.FLASH if effect_list: - self._effect_list = effect_list + self._attr_effect_list = effect_list self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, @@ -489,11 +647,29 @@ class Light(BaseLight, ZhaEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_enhanced_light_transition = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, + False, + ) + self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, + True, + ) @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" - self._state = bool(value) + if self._transitioning: + self.debug( + "received onoff %s while transitioning - skipping update", + value, + ) + return + self._attr_state = bool(value) if value: self._off_with_transition = False self._off_brightness = None @@ -520,6 +696,38 @@ class Light(BaseLight, ZhaEntity): signal_override=True, ) + @callback + def transition_on(signal): + """Handle a transition start event from a group.""" + if self.entity_id in signal["entity_ids"]: + self.debug( + "group transition started - setting member transitioning flag" + ) + self._transitioning = True + + self.async_accept_signal( + None, + SIGNAL_LIGHT_GROUP_TRANSITION_START, + transition_on, + signal_override=True, + ) + + @callback + def transition_off(signal): + """Handle a transition finished event from a group.""" + if self.entity_id in signal["entity_ids"]: + self.debug( + "group transition completed - unsetting member transitioning flag" + ) + self._transitioning = False + + self.async_accept_signal( + None, + SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED, + transition_off, + signal_override=True, + ) + async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" assert self._cancel_refresh_handle @@ -529,9 +737,9 @@ class Light(BaseLight, ZhaEntity): @callback def async_restore_last_state(self, last_state): """Restore previous state.""" - self._state = last_state.state == STATE_ON + self._attr_state = last_state.state == STATE_ON if "brightness" in last_state.attributes: - self._brightness = last_state.attributes["brightness"] + self._attr_brightness = last_state.attributes["brightness"] if "off_with_transition" in last_state.attributes: self._off_with_transition = last_state.attributes["off_with_transition"] if "off_brightness" in last_state.attributes: @@ -539,15 +747,17 @@ class Light(BaseLight, ZhaEntity): if "color_mode" in last_state.attributes: self._attr_color_mode = ColorMode(last_state.attributes["color_mode"]) if "color_temp" in last_state.attributes: - self._color_temp = last_state.attributes["color_temp"] + self._attr_color_temp = last_state.attributes["color_temp"] + if "xy_color" in last_state.attributes: + self._attr_xy_color = last_state.attributes["xy_color"] if "hs_color" in last_state.attributes: - self._hs_color = last_state.attributes["hs_color"] + self._attr_hs_color = last_state.attributes["hs_color"] if "effect" in last_state.attributes: - self._effect = last_state.attributes["effect"] + self._attr_effect = last_state.attributes["effect"] async def async_get_state(self): """Attempt to retrieve the state from the light.""" - if not self.available: + if not self._attr_available: return self.debug("polling current state") if self._on_off_channel: @@ -555,21 +765,36 @@ class Light(BaseLight, ZhaEntity): "on_off", from_cache=False ) if state is not None: - self._state = state + self._attr_state = state if self._level_channel: level = await self._level_channel.get_attribute_value( "current_level", from_cache=False ) if level is not None: - self._brightness = level + self._attr_brightness = level if self._color_channel: attributes = [ "color_mode", - "color_temperature", "current_x", "current_y", - "color_loop_active", ] + if ( + not self._zha_config_always_prefer_xy_color_mode + and self._color_channel.enhanced_hue_supported + ): + attributes.append("enhanced_current_hue") + attributes.append("current_saturation") + if ( + self._color_channel.hs_supported + and not self._color_channel.enhanced_hue_supported + and not self._zha_config_always_prefer_xy_color_mode + ): + attributes.append("current_hue") + attributes.append("current_saturation") + if self._color_channel.color_temp_supported: + attributes.append("color_temperature") + if self._color_channel.color_loop_supported: + attributes.append("color_loop_active") results = await self._color_channel.get_attributes( attributes, from_cache=False, only_cache=False @@ -580,37 +805,65 @@ class Light(BaseLight, ZhaEntity): self._attr_color_mode = ColorMode.COLOR_TEMP color_temp = results.get("color_temperature") if color_temp is not None and color_mode: - self._color_temp = color_temp - self._hs_color = None - else: + self._attr_color_temp = color_temp + self._attr_xy_color = None + self._attr_hs_color = None + elif ( + color_mode == Color.ColorMode.Hue_and_saturation + and not self._zha_config_always_prefer_xy_color_mode + ): self._attr_color_mode = ColorMode.HS + if self._color_channel.enhanced_hue_supported: + current_hue = results.get("enhanced_current_hue") + else: + current_hue = results.get("current_hue") + current_saturation = results.get("current_saturation") + if current_hue is not None and current_saturation is not None: + self._attr_hs_color = ( + int(current_hue * 360 / 65535) + if self._color_channel.enhanced_hue_supported + else int(current_hue * 360 / 254), + int(current_saturation / 2.54), + ) + self._attr_xy_color = None + self._attr_color_temp = None + else: + self._attr_color_mode = ColorMode.XY color_x = results.get("current_x") color_y = results.get("current_y") if color_x is not None and color_y is not None: - self._hs_color = color_util.color_xy_to_hs( - float(color_x / 65535), float(color_y / 65535) - ) - self._color_temp = None + self._attr_xy_color = (color_x / 65535, color_y / 65535) + self._attr_color_temp = None + self._attr_hs_color = None color_loop_active = results.get("color_loop_active") if color_loop_active is not None: if color_loop_active == 1: - self._effect = light.EFFECT_COLORLOOP + self._attr_effect = light.EFFECT_COLORLOOP else: - self._effect = None + self._attr_effect = None async def async_update(self): """Update to the latest state.""" + if self._transitioning: + self.debug("skipping async_update while transitioning") + return await self.async_get_state() async def _refresh(self, time): """Call async_get_state at an interval.""" + if self._transitioning: + self.debug("skipping _refresh while transitioning") + return await self.async_get_state() self.async_write_ha_state() async def _maybe_force_refresh(self, signal): """Force update the state if the signal contains the entity id for this entity.""" if self.entity_id in signal["entity_ids"]: + if self._transitioning: + self.debug("skipping _maybe_force_refresh while transitioning") + return await self.async_get_state() self.async_write_ha_state() @@ -640,10 +893,10 @@ class ForceOnLight(Light): @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, - manufacturers={"Sengled"}, + manufacturers=DEFAULT_MIN_TRANSITION_MANUFACTURERS, ) -class SengledLight(Light): - """Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition.""" +class MinTransitionLight(Light): + """Representation of a light which does not react to any "move to" calls with 0 as a transition.""" _DEFAULT_MIN_TRANSITION_TIME = 1 @@ -658,6 +911,10 @@ class LightGroup(BaseLight, ZhaGroupEntity): """Initialize a light group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) group = self.zha_device.gateway.get_group(self._group_id) + self._DEFAULT_MIN_TRANSITION_TIME = any( # pylint: disable=invalid-name + member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS + for member in group.members + ) self._on_off_channel = group.endpoint[OnOff.cluster_id] self._level_channel = group.endpoint[LevelControl.cluster_id] self._color_channel = group.endpoint[Color.cluster_id] @@ -669,8 +926,27 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, + True, + ) + self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + True, + ) + self._zha_config_enhanced_light_transition = False self._attr_color_mode = None + # remove this when all ZHA platforms and base entities are updated + @property + def available(self) -> bool: + """Return entity availability.""" + return self._attr_available + async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() @@ -687,52 +963,82 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" await super().async_turn_on(**kwargs) + if self._transitioning: + return await self._debounced_member_refresh.async_call() async def async_turn_off(self, **kwargs): """Turn the entity off.""" await super().async_turn_off(**kwargs) + if self._transitioning: + return await self._debounced_member_refresh.async_call() + @callback + def async_state_changed_listener(self, event: Event): + """Handle child updates.""" + if self._transitioning: + self.debug("skipping group entity state update during transition") + return + super().async_state_changed_listener(event) + + async def async_update_ha_state(self, force_refresh: bool = False) -> None: + """Update Home Assistant with current state of entity.""" + if self._transitioning: + self.debug("skipping group entity state update during transition") + return + await super().async_update_ha_state(force_refresh) + async def async_update(self) -> None: """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] - self._state = len(on_states) > 0 - self._available = any(state.state != STATE_UNAVAILABLE for state in states) + self._attr_state = len(on_states) > 0 + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) - self._brightness = helpers.reduce_attribute(on_states, ATTR_BRIGHTNESS) - - self._hs_color = helpers.reduce_attribute( - on_states, ATTR_HS_COLOR, reduce=helpers.mean_tuple + self._attr_brightness = helpers.reduce_attribute( + on_states, light.ATTR_BRIGHTNESS ) - self._color_temp = helpers.reduce_attribute(on_states, ATTR_COLOR_TEMP) - self._min_mireds = helpers.reduce_attribute( - states, ATTR_MIN_MIREDS, default=153, reduce=min - ) - self._max_mireds = helpers.reduce_attribute( - states, ATTR_MAX_MIREDS, default=500, reduce=max + self._attr_xy_color = helpers.reduce_attribute( + on_states, light.ATTR_XY_COLOR, reduce=helpers.mean_tuple ) - self._effect_list = None - all_effect_lists = list(helpers.find_state_attributes(states, ATTR_EFFECT_LIST)) + if not self._zha_config_always_prefer_xy_color_mode: + self._attr_hs_color = helpers.reduce_attribute( + on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple + ) + + self._attr_color_temp = helpers.reduce_attribute( + on_states, light.ATTR_COLOR_TEMP + ) + self._attr_min_mireds = helpers.reduce_attribute( + states, light.ATTR_MIN_MIREDS, default=153, reduce=min + ) + self._attr_max_mireds = helpers.reduce_attribute( + states, light.ATTR_MAX_MIREDS, default=500, reduce=max + ) + + self._attr_effect_list = None + all_effect_lists = list( + helpers.find_state_attributes(states, light.ATTR_EFFECT_LIST) + ) if all_effect_lists: # Merge all effects from all effect_lists with a union merge. - self._effect_list = list(set().union(*all_effect_lists)) + self._attr_effect_list = list(set().union(*all_effect_lists)) - self._effect = None - all_effects = list(helpers.find_state_attributes(on_states, ATTR_EFFECT)) + self._attr_effect = None + all_effects = list(helpers.find_state_attributes(on_states, light.ATTR_EFFECT)) if all_effects: # Report the most common effect. effects_count = Counter(itertools.chain(all_effects)) - self._effect = effects_count.most_common(1)[0][0] + self._attr_effect = effects_count.most_common(1)[0][0] self._attr_color_mode = None all_color_modes = list( - helpers.find_state_attributes(on_states, ATTR_COLOR_MODE) + helpers.find_state_attributes(on_states, light.ATTR_COLOR_MODE) ) if all_color_modes: # Report the most common color mode, select brightness and onoff last @@ -742,10 +1048,15 @@ class LightGroup(BaseLight, ZhaGroupEntity): if ColorMode.BRIGHTNESS in color_mode_count: color_mode_count[ColorMode.BRIGHTNESS] = 0 self._attr_color_mode = color_mode_count.most_common(1)[0][0] + if self._attr_color_mode == ColorMode.HS and ( + color_mode_count[ColorMode.HS] != len(self._group.members) + or self._zha_config_always_prefer_xy_color_mode + ): # switch to XY if all members do not support HS + self._attr_color_mode = ColorMode.XY self._attr_supported_color_modes = None all_supported_color_modes = list( - helpers.find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) + helpers.find_state_attributes(states, light.ATTR_SUPPORTED_COLOR_MODES) ) if all_supported_color_modes: # Merge all color modes. @@ -753,14 +1064,14 @@ class LightGroup(BaseLight, ZhaGroupEntity): set[str], set().union(*all_supported_color_modes) ) - self._supported_features = 0 + self._attr_supported_features = 0 for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature # we find. - self._supported_features |= support + self._attr_supported_features |= support # Bitwise-and the supported features with the GroupedLight's features # so that we don't break in the future when a new feature is added. - self._supported_features &= SUPPORT_GROUP_LIGHT + self._attr_supported_features &= SUPPORT_GROUP_LIGHT async def _force_member_updates(self): """Force the update of member entities to ensure the states are correct for bulbs that don't report their state.""" diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c43958f7e46..179302af1cd 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.1", + "bellows==0.31.2", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.77", + "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.47.3", + "zigpy==0.48.0", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", "zigpy-znp==0.8.1" @@ -76,7 +76,7 @@ "known_devices": ["Bitron Video AV2010/10"] } ], - "codeowners": ["@dmulcahey", "@adminiuga"], + "codeowners": ["@dmulcahey", "@adminiuga", "@puddly"], "zeroconf": [ { "type": "_esphomelib._tcp.local.", diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 36fc5267bd9..4252bf0e14c 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.foundation import Status @@ -33,6 +33,10 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +_ZHANumberConfigurationEntitySelfT = TypeVar( + "_ZHANumberConfigurationEntitySelfT", bound="ZHANumberConfigurationEntity" +) + STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.NUMBER) CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.NUMBER @@ -368,12 +372,12 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): @classmethod def create_entity( - cls, + cls: type[_ZHANumberConfigurationEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZHANumberConfigurationEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -397,7 +401,7 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this number configuration entity.""" self._channel: ZigbeeChannel = channels[0] @@ -521,7 +525,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0x257 - _attr_unit_of_measurement: str | None = UNITS[72] + _attr_native_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "timer_duration" @@ -533,5 +537,5 @@ class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time") _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0xFFFFFFFF - _attr_unit_of_measurement: str | None = UNITS[72] + _attr_native_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "filter_life_time" diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 79349273e38..503c5a013a8 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum import functools import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeVar from zigpy import types from zigpy.zcl.clusters.general import OnOff @@ -34,6 +34,10 @@ if TYPE_CHECKING: from .core.device import ZHADevice +_ZCLEnumSelectEntitySelfT = TypeVar( + "_ZCLEnumSelectEntitySelfT", bound="ZCLEnumSelectEntity" +) + CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.SELECT ) @@ -72,7 +76,7 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this select entity.""" self._attr_name = self._enum.__name__ @@ -154,12 +158,12 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): @classmethod def create_entity( - cls, + cls: type[_ZCLEnumSelectEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZCLEnumSelectEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -183,7 +187,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this select entity.""" self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] @@ -264,3 +268,20 @@ class AqaraApproachDistance(ZCLEnumSelectEntity, id_suffix="approach_distance"): _select_attr = "approach_distance" _enum = AqaraApproachDistances + + +class AqaraE1ReverseDirection(types.enum8): + """Aqara curtain reversal.""" + + Normal = 0x00 + Inverted = 0x01 + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="window_covering", models={"lumi.curtain.agl001"} +) +class AqaraCurtainMode(ZCLEnumSelectEntity, id_suffix="window_covering_mode"): + """Representation of a ZHA curtain mode configuration entity.""" + + _select_attr = "window_covering_mode" + _enum = AqaraE1ReverseDirection diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 513ba5510b5..f42e88041ef 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import numbers -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from homeassistant.components.climate.const import HVACAction from homeassistant.components.sensor import ( @@ -69,6 +69,13 @@ if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel from .core.device import ZHADevice +_SensorSelfT = TypeVar("_SensorSelfT", bound="Sensor") +_BatterySelfT = TypeVar("_BatterySelfT", bound="Battery") +_ThermostatHVACActionSelfT = TypeVar( + "_ThermostatHVACActionSelfT", bound="ThermostatHVACAction" +) +_RSSISensorSelfT = TypeVar("_RSSISensorSelfT", bound="RSSISensor") + PARALLEL_UPDATES = 5 BATTERY_SIZES = { @@ -126,7 +133,7 @@ class Sensor(ZhaEntity, SensorEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -134,12 +141,12 @@ class Sensor(ZhaEntity, SensorEntity): @classmethod def create_entity( - cls, + cls: type[_SensorSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _SensorSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -214,12 +221,12 @@ class Battery(Sensor): @classmethod def create_entity( - cls, + cls: type[_BatterySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _BatterySelfT | None: """Entity Factory. Unlike any other entity, PowerConfiguration cluster may not support @@ -641,12 +648,12 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): @classmethod def create_entity( - cls, + cls: type[_ThermostatHVACActionSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ThermostatHVACActionSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -767,12 +774,12 @@ class RSSISensor(Sensor, id_suffix="rssi"): @classmethod def create_entity( - cls, + cls: type[_RSSISensorSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _RSSISensorSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 5953df52e92..4eb872f4fae 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -37,6 +37,9 @@ "config_panel": { "zha_options": { "title": "Global Options", + "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", + "light_transitioning_flag": "Enable enhanced brightness slider during light transition", + "always_prefer_xy_color_mode": "Always prefer XY color mode", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 3b044bb7646..881401f31da 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.clusters.general import OnOff @@ -31,6 +31,10 @@ if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel from .core.device import ZHADevice +_ZHASwitchConfigurationEntitySelfT = TypeVar( + "_ZHASwitchConfigurationEntitySelfT", bound="ZHASwitchConfigurationEntity" +) + STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.SWITCH) CONFIG_DIAGNOSTIC_MATCH = functools.partial( @@ -172,12 +176,12 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): @classmethod def create_entity( - cls, + cls: type[_ZHASwitchConfigurationEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], **kwargs: Any, - ) -> ZhaEntity | None: + ) -> _ZHASwitchConfigurationEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 2fe8d1a151c..4167704e4a0 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Considera els dispositius connectats a la xarxa el\u00e8ctrica com a no disponibles al cap de (segons)", "default_light_transition": "Temps de transici\u00f3 predeterminat (segons)", "enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa", + "enhanced_light_transition": "Activa la transici\u00f3 millorada de color/temperatura de llum des de l'estat apagat", "title": "Opcions globals" } }, diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 88638c6c696..b4ad58636a7 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -46,10 +46,13 @@ "title": "Optionen f\u00fcr die Alarmsteuerung" }, "zha_options": { + "always_prefer_xy_color_mode": "Immer den XY-Farbmodus bevorzugen", "consider_unavailable_battery": "Batteriebetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", + "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", + "light_transitioning_flag": "Erweiterten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } }, diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index e0fb76cd6cb..e0e063df426 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "\u0398\u03b5\u03c9\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03c9\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "default_light_transition": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "enable_identify_on_join": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03c6\u03ad \u03b1\u03bd\u03b1\u03b3\u03bd\u03ce\u03c1\u03b9\u03c3\u03b7\u03c2 \u03cc\u03c4\u03b1\u03bd \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "enhanced_light_transition": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b2\u03b5\u03bb\u03c4\u03b9\u03c9\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2/\u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", "title": "\u039a\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03ad\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" } }, diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 00c78101a53..757ab338ec6 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -46,10 +46,13 @@ "title": "Alarm Control Panel Options" }, "zha_options": { + "always_prefer_xy_color_mode": "Always prefer XY color mode", "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", + "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", + "light_transitioning_flag": "Enable enhanced brightness slider during light transition", "title": "Global Options" } }, diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 16ab4a84b6d..567ee5f359c 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Arvesta, et v\u00f5rgutoitega seadmed pole p\u00e4rast (sekundit) saadaval", "default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)", "enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga", + "enhanced_light_transition": "Luba t\u00e4iustatud valguse v\u00e4rvi/temperatuuri \u00fcleminek v\u00e4ljal\u00fclitatud olekust", "title": "\u00dcldised valikud" } }, diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index d446de8e17a..61b0f0c0c9d 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "H\u00e1l\u00f3zati t\u00e1pell\u00e1t\u00e1s\u00fa eszk\u00f6z\u00f6k nem el\u00e9rhet\u0151 \u00e1llapot\u00faak ennyi id\u0151 ut\u00e1n (mp.)", "default_light_transition": "Alap\u00e9rtelmezett f\u00e9ny-\u00e1tmeneti id\u0151 (m\u00e1sodpercben)", "enable_identify_on_join": "Azonos\u00edt\u00f3 hat\u00e1s, amikor az eszk\u00f6z\u00f6k csatlakoznak a h\u00e1l\u00f3zathoz", + "enhanced_light_transition": "F\u00e9ny sz\u00edn/sz\u00ednh\u0151m\u00e9rs\u00e9klet \u00e1tmenete kikapcsolt \u00e1llapotb\u00f3l", "title": "Glob\u00e1lis be\u00e1ll\u00edt\u00e1sok" } }, diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 5a10e3d01af..63eee2f376b 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Anggap perangkat bertenaga listrik sebagai tidak tersedia setelah (detik)", "default_light_transition": "Waktu transisi lampu default (detik)", "enable_identify_on_join": "Aktifkan efek identifikasi saat perangkat bergabung dengan jaringan", + "enhanced_light_transition": "Aktifkan versi canggih untuk transisi warna/suhu cahaya dari keadaan tidak aktif", "title": "Opsi Global" } }, diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 57be4c7acb6..4666ba7e494 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -46,10 +46,12 @@ "title": "Opzioni del pannello di controllo degli allarmi" }, "zha_options": { + "always_prefer_xy_color_mode": "Preferisci sempre la modalit\u00e0 colore XY", "consider_unavailable_battery": "Considera i dispositivi alimentati a batteria non disponibili dopo (secondi)", "consider_unavailable_mains": "Considera i dispositivi alimentati dalla rete non disponibili dopo (secondi)", "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", "enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete", + "enhanced_light_transition": "Abilita una transizione migliorata del colore/temperatura della luce da uno stato spento", "title": "Opzioni globali" } }, diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 636df6f047e..14ba0b9280f 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_zha_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fzha\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "usb_probe_failed": "USB\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u51fa\u3059\u3053\u3068\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { @@ -50,6 +50,7 @@ "consider_unavailable_mains": "(\u79d2)\u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", "default_light_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e9\u30a4\u30c8\u9077\u79fb\u6642\u9593(\u79d2)", "enable_identify_on_join": "\u30c7\u30d0\u30a4\u30b9\u304c\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u53c2\u52a0\u3059\u308b\u969b\u306b\u3001\u8b58\u5225\u52b9\u679c\u3092\u6709\u52b9\u306b\u3059\u308b", + "enhanced_light_transition": "\u30aa\u30d5\u72b6\u614b\u304b\u3089\u3001\u30a8\u30f3\u30cf\u30f3\u30b9\u30c9\u30e9\u30a4\u30c8\u30ab\u30e9\u30fc/\u8272\u6e29\u5ea6\u3078\u306e\u9077\u79fb\u3092\u6709\u52b9\u306b\u3057\u307e\u3059", "title": "\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d7\u30b7\u30e7\u30f3" } }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 6c67c6aea93..99c519782db 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -46,10 +46,13 @@ "title": "Opcje panelu alarmowego" }, "zha_options": { + "always_prefer_xy_color_mode": "Zawsze preferuj tryb kolor\u00f3w XY", "consider_unavailable_battery": "Uznaj urz\u0105dzenia zasilane bateryjnie za niedost\u0119pne po (sekundach)", "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z gniazdka za niedost\u0119pne po (sekundach)", "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", + "enhanced_light_transition": "W\u0142\u0105cz ulepszone przej\u015bcie koloru \u015bwiat\u0142a/temperatury ze stanu wy\u0142\u0105czenia", + "light_transitioning_flag": "W\u0142\u0105cz suwak zwi\u0119kszonej jasno\u015bci podczas przej\u015bcia \u015bwiat\u0142a", "title": "Opcje og\u00f3lne" } }, diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 8c43bf7f3e6..69b8ced6970 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -46,10 +46,13 @@ "title": "Op\u00e7\u00f5es do painel de controle de alarme" }, "zha_options": { + "always_prefer_xy_color_mode": "Sempre prefira o modo de cor XY", "consider_unavailable_battery": "Considerar dispositivos alimentados por bateria indispon\u00edveis ap\u00f3s (segundos)", "consider_unavailable_mains": "Considerar os dispositivos alimentados pela rede indispon\u00edveis ap\u00f3s (segundos)", "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz padr\u00e3o (segundos)", "enable_identify_on_join": "Ativar o efeito de identifica\u00e7\u00e3o quando os dispositivos ingressarem na rede", + "enhanced_light_transition": "Ative a transi\u00e7\u00e3o de cor/temperatura da luz aprimorada de um estado desligado", + "light_transitioning_flag": "Ative o controle deslizante de brilho aprimorado durante a transi\u00e7\u00e3o de luz", "title": "Op\u00e7\u00f5es globais" } }, diff --git a/homeassistant/components/zha/translations/pt.json b/homeassistant/components/zha/translations/pt.json index 42a5c292ecb..435e80b8b76 100644 --- a/homeassistant/components/zha/translations/pt.json +++ b/homeassistant/components/zha/translations/pt.json @@ -12,6 +12,11 @@ } } }, + "config_panel": { + "zha_options": { + "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz predefinido (segundos)" + } + }, "device_automation": { "action_type": { "warn": "Avisar" diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index f9d67cc2d35..83ba818087a 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -40,16 +40,17 @@ }, "config_panel": { "zha_alarm_options": { - "alarm_arm_requires_code": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "alarm_arm_requires_code": "\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", "alarm_failed_tries": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043d\u0435\u0443\u0434\u0430\u0447\u043d\u044b\u0445 \u0432\u0432\u043e\u0434\u043e\u0432 \u043a\u043e\u0434\u0430, \u0434\u043b\u044f \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0442\u0440\u0435\u0432\u043e\u0433\u0438", "alarm_master_code": "\u041c\u0430\u0441\u0442\u0435\u0440-\u043a\u043e\u0434 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u043d\u0435\u043b\u0435\u0439", - "title": "\u041e\u043f\u0446\u0438\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u0435\u0439 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" }, "zha_options": { - "consider_unavailable_battery": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0441\u0435\u043a\u0443\u043d\u0434)", - "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0441\u0435\u043a\u0443\u043d\u0434)", + "consider_unavailable_battery": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", + "enhanced_light_transition": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u0446\u0432\u0435\u0442\u0430/\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0441\u0432\u0435\u0442\u0430 \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" } }, diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 9bf7d3c9208..546a2f77c31 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -46,10 +46,13 @@ "title": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u9078\u9805" }, "zha_options": { + "always_prefer_xy_color_mode": "\u504f\u597d XY \u8272\u5f69\u6a21\u5f0f", "consider_unavailable_battery": "\u5c07\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "consider_unavailable_mains": "\u5c07\u4e3b\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", + "enhanced_light_transition": "\u958b\u555f\u7531\u95dc\u9589\u72c0\u614b\u589e\u5f37\u5149\u8272/\u8272\u6eab\u8f49\u63db", + "light_transitioning_flag": "\u958b\u555f\u71c8\u5149\u8f49\u63db\u589e\u5f37\u4eae\u5ea6\u8abf\u6574\u5217", "title": "Global \u9078\u9805" } }, diff --git a/homeassistant/components/zodiac/translations/sensor.pt.json b/homeassistant/components/zodiac/translations/sensor.pt.json new file mode 100644 index 00000000000..a5e943a5daf --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.pt.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Aqu\u00e1rio", + "aries": "Carneiro", + "cancer": "Caranguejo", + "capricorn": "Capric\u00f3rnio", + "gemini": "G\u00e9meos", + "leo": "Le\u00e3o", + "libra": "Balan\u00e7a", + "pisces": "Peixes", + "sagittarius": "Sagit\u00e1rio", + "scorpio": "Escorpi\u00e3o", + "taurus": "Touro", + "virgo": "Virgem" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/bg.json b/homeassistant/components/zoneminder/translations/bg.json index a19fe59b023..ad4605eceb9 100644 --- a/homeassistant/components/zoneminder/translations/bg.json +++ b/homeassistant/components/zoneminder/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "auth_fail": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0430 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0438.", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "error": { "auth_fail": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0430 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0438.", diff --git a/homeassistant/components/zoneminder/translations/pt.json b/homeassistant/components/zoneminder/translations/pt.json index f8fa0efe967..b85c7c2e7a7 100644 --- a/homeassistant/components/zoneminder/translations/pt.json +++ b/homeassistant/components/zoneminder/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "auth_fail": "O nome de utilizador ou palavra-passe est\u00e1 incorrecto.", "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index f6480689910..7fa20b2b1f5 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -90,7 +90,7 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = # NotificationType 2: Carbon Monoxide - State Id's 1 and 2 key=NOTIFICATION_CARBON_MONOOXIDE, states=("1", "2"), - device_class=BinarySensorDeviceClass.GAS, + device_class=BinarySensorDeviceClass.CO, ), NotificationZWaveJSEntityDescription( # NotificationType 2: Carbon Monoxide - All other State Id's diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 7775995437a..c42b5af71c4 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -196,7 +196,6 @@ def async_condition_from_config( raise HomeAssistantError(f"Unhandled condition type {condition_type}") -@callback async def async_get_condition_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index d1097a6cd65..8b6ecefc5f5 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -1,6 +1,6 @@ { "domain": "zwave_js", - "name": "Z-Wave JS", + "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", "requirements": ["zwave-js-server-python==0.39.0"], @@ -30,5 +30,8 @@ } ], "zeroconf": ["_zwave-js-server._tcp.local."], - "loggers": ["zwave_js_server"] + "loggers": ["zwave_js_server"], + "supported_brands": { + "leviton_z_wave": "Leviton Z-Wave" + } } diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 37e19e5471f..f93ff278c5f 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -100,7 +100,7 @@ "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Nem siker\u00fclt csatlakozni", - "different_device": "A csatlakoztatott USB-eszk\u00f6z nem ugyanaz, mint amelyet kor\u00e1bban ehhez a konfigur\u00e1ci\u00f3s bejegyz\u00e9shez konfigur\u00e1ltak. K\u00e9rj\u00fck, ink\u00e1bb hozzon l\u00e9tre egy \u00faj konfigur\u00e1ci\u00f3s bejegyz\u00e9st az \u00faj eszk\u00f6zh\u00f6z." + "different_device": "A csatlakoztatott USB-eszk\u00f6z nem ugyanaz, mint amelyet kor\u00e1bban ehhez a konfigur\u00e1ci\u00f3s bejegyz\u00e9shez konfigur\u00e1ltak. K\u00e9rem, ink\u00e1bb hozzon l\u00e9tre egy \u00faj konfigur\u00e1ci\u00f3s bejegyz\u00e9st az \u00faj eszk\u00f6zh\u00f6z." }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index f4c5a62b050..94645e302f4 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -36,7 +36,7 @@ "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b" }, "hassio_confirm": { - "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "install_addon": { "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f\u3002" diff --git a/homeassistant/components/zwave_js/translations/pt.json b/homeassistant/components/zwave_js/translations/pt.json new file mode 100644 index 00000000000..dc59810f31e --- /dev/null +++ b/homeassistant/components/zwave_js/translations/pt.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + }, + "options": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emular Hardware" + } + }, + "manual": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index f69bbc1eea1..04627583d0e 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave.Me", "documentation": "https://www.home-assistant.io/integrations/zwave_me", "iot_class": "local_push", - "requirements": ["zwave_me_ws==0.2.4", "url-normalize==1.4.1"], + "requirements": ["zwave_me_ws==0.2.4", "url-normalize==1.4.3"], "after_dependencies": ["zeroconf"], "zeroconf": [{ "type": "_hap._tcp.local.", "name": "*z.wave-me*" }], "config_flow": true, diff --git a/homeassistant/components/zwave_me/translations/de.json b/homeassistant/components/zwave_me/translations/de.json index 747ccf0c9c8..6e20c28ce07 100644 --- a/homeassistant/components/zwave_me/translations/de.json +++ b/homeassistant/components/zwave_me/translations/de.json @@ -13,7 +13,7 @@ "token": "API-Token", "url": "URL" }, - "description": "Gib die IP-Adresse mit Port und Zugangs-Token des Z-Way-Servers ein. Um das Token zu erhalten, gehe zur Z-Way-Benutzeroberfl\u00e4che Smart Home UI > Men\u00fc > Einstellungen > Benutzer > Administrator > API-Token.\n\nBeispiel f\u00fcr die Verbindung zu Z-Way im lokalen Netzwerk:\nURL: {local_url}\nToken: {local_token}\n\nBeispiel f\u00fcr die Verbindung zu Z-Way \u00fcber den Fernzugriff find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nBeispiel f\u00fcr eine Verbindung zu Z-Way mit einer statischen \u00f6ffentlichen IP-Adresse:\nURL: {remote_url}\nToken: {local_token}\n\nWenn du dich \u00fcber find.z-wave.me verbindest, musst du ein Token mit globalem Geltungsbereich verwenden (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." + "description": "Gib die IP-Adresse mit Port und Zugangs-Token des Z-Way-Servers ein. Um das Token zu erhalten, gehe zur Z-Way-Benutzeroberfl\u00e4che Smart Home UI \u2192 Men\u00fc \u2192 Einstellungen \u2192 Benutzer \u2192 Administrator \u2192 API-Token.\n\nBeispiel f\u00fcr die Verbindung zu Z-Way im lokalen Netzwerk:\nURL: {local_url}\nToken: {local_token}\n\nBeispiel f\u00fcr die Verbindung zu Z-Way \u00fcber den Fernzugriff find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nBeispiel f\u00fcr eine Verbindung zu Z-Way mit einer statischen \u00f6ffentlichen IP-Adresse:\nURL: {remote_url}\nToken: {local_token}\n\nWenn du dich \u00fcber find.z-wave.me verbindest, musst du ein Token mit globalem Geltungsbereich verwenden (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." } } } diff --git a/homeassistant/components/zwave_me/translations/pt.json b/homeassistant/components/zwave_me/translations/pt.json new file mode 100644 index 00000000000..a142354a462 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "token": "API Token", + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/config.py b/homeassistant/config.py index 5c870918231..91f94bbbf40 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -286,6 +286,7 @@ async def async_create_default_config(hass: HomeAssistant) -> bool: Return if creation was successful. """ + assert hass.config.config_dir return await hass.async_add_executor_job( _write_default_config, hass.config.config_dir ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c832bab7eb4..7c2f1c84ff9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -5,7 +5,6 @@ import asyncio from collections import ChainMap from collections.abc import Callable, Coroutine, Iterable, Mapping from contextvars import ContextVar -import dataclasses from enum import Enum import functools import logging @@ -28,15 +27,17 @@ from .util import uuid as uuid_util from .util.decorator import Registry if TYPE_CHECKING: + from .components.bluetooth import BluetoothServiceInfoBleak from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo - from .components.mqtt import MqttServiceInfo from .components.ssdp import SsdpServiceInfo from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo + from .helpers.service_info.mqtt import MqttServiceInfo _LOGGER = logging.getLogger(__name__) +SOURCE_BLUETOOTH = "bluetooth" SOURCE_DHCP = "dhcp" SOURCE_DISCOVERY = "discovery" SOURCE_HASSIO = "hassio" @@ -72,7 +73,7 @@ PATH_CONFIG = ".config_entries.json" SAVE_DELAY = 1 -_T = TypeVar("_T", bound="ConfigEntryState") +_ConfigEntryStateSelfT = TypeVar("_ConfigEntryStateSelfT", bound="ConfigEntryState") _R = TypeVar("_R") @@ -96,7 +97,9 @@ class ConfigEntryState(Enum): _recoverable: bool - def __new__(cls: type[_T], value: str, recoverable: bool) -> _T: + def __new__( + cls: type[_ConfigEntryStateSelfT], value: str, recoverable: bool + ) -> _ConfigEntryStateSelfT: """Create new ConfigEntryState.""" obj = object.__new__(cls) obj._value_ = value @@ -116,6 +119,7 @@ class ConfigEntryState(Enum): DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id" DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" DISCOVERY_SOURCES = { + SOURCE_BLUETOOTH, SOURCE_DHCP, SOURCE_DISCOVERY, SOURCE_HOMEKIT, @@ -636,7 +640,12 @@ class ConfigEntry: await asyncio.gather(*pending) @callback - def async_start_reauth(self, hass: HomeAssistant) -> None: + def async_start_reauth( + self, + hass: HomeAssistant, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> None: """Start a reauth flow.""" flow_context = { "source": SOURCE_REAUTH, @@ -645,6 +654,9 @@ class ConfigEntry: "unique_id": self.unique_id, } + if context: + flow_context.update(context) + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain): if flow["context"] == flow_context: return @@ -653,7 +665,7 @@ class ConfigEntry: hass.config_entries.flow.async_init( self.domain, context=flow_context, - data=self.data, + data=self.data | (data or {}), ) ) @@ -842,7 +854,9 @@ class ConfigEntries: self._hass_config = hass_config self._entries: dict[str, ConfigEntry] = {} self._domain_index: dict[str, list[str]] = {} - self._store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = storage.Store[dict[str, list[dict[str, Any]]]]( + hass, STORAGE_VERSION, STORAGE_KEY + ) EntityRegistryDisabledHandler(hass).async_setup() @callback @@ -1155,9 +1169,24 @@ class ConfigEntries: self, entry: ConfigEntry, platforms: Iterable[Platform | str] ) -> None: """Forward the setup of an entry to platforms.""" + report( + "called async_setup_platforms instead of awaiting async_forward_entry_setups; " + "this will fail in version 2022.12", + # Raise this to warning once all core integrations have been migrated + level=logging.DEBUG, + error_if_core=False, + ) for platform in platforms: self.hass.async_create_task(self.async_forward_entry_setup(entry, platform)) + async def async_forward_entry_setups( + self, entry: ConfigEntry, platforms: Iterable[Platform | str] + ) -> None: + """Forward the setup of an entry to platforms.""" + await asyncio.gather( + *(self.async_forward_entry_setup(entry, platform) for platform in platforms) + ) + async def async_forward_entry_setup( self, entry: ConfigEntry, domain: Platform | str ) -> bool: @@ -1166,9 +1195,6 @@ class ConfigEntries: By default an entry is setup with the component it belongs to. If that component also has related platforms, the component will have to forward the entry to be setup by that component. - - You don't want to await this coroutine if it is called as part of the - setup of a component, because it can cause a deadlock. """ # Setup Component if not set up yet if domain not in self.hass.config.components: @@ -1429,13 +1455,19 @@ class ConfigFlow(data_entry_flow.FlowHandler): if self._async_in_progress(include_uninitialized=True): raise data_entry_flow.AbortFlow("already_in_progress") - async def async_step_discovery( - self, discovery_info: DiscoveryInfoType + async def _async_step_discovery_without_unique_id( + self, ) -> data_entry_flow.FlowResult: """Handle a flow initialized by discovery.""" await self._async_handle_discovery_without_unique_id() return await self.async_step_user() + async def async_step_discovery( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by discovery.""" + return await self._async_step_discovery_without_unique_id() + @callback def async_abort( self, @@ -1460,53 +1492,59 @@ class ConfigFlow(data_entry_flow.FlowHandler): reason=reason, description_placeholders=description_placeholders ) + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by Bluetooth discovery.""" + return await self._async_step_discovery_without_unique_id() + async def async_step_dhcp( self, discovery_info: DhcpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by DHCP discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_hassio( self, discovery_info: HassioServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by HASS IO discovery.""" - return await self.async_step_discovery(discovery_info.config) + return await self._async_step_discovery_without_unique_id() async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> data_entry_flow.FlowResult: """Handle a flow initialized by integration specific discovery.""" - return await self.async_step_discovery(discovery_info) + return await self._async_step_discovery_without_unique_id() async def async_step_homekit( self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Homekit discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_mqtt( self, discovery_info: MqttServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by MQTT discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_ssdp( self, discovery_info: SsdpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by SSDP discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_usb( self, discovery_info: UsbServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by USB discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Zeroconf discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() @callback def async_create_entry( diff --git a/homeassistant/const.py b/homeassistant/const.py index 71d30ae608d..18561a8bd2e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,8 +6,8 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "7" +MINOR_VERSION: Final = 8 +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/homeassistant/core.py b/homeassistant/core.py index b568ee72689..7b41fe476aa 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1942,7 +1942,7 @@ class Config: # pylint: disable=import-outside-toplevel from .helpers.storage import Store - store = Store( + store = Store[dict[str, Any]]( self.hass, CORE_STORAGE_VERSION, CORE_STORAGE_KEY, @@ -1950,7 +1950,7 @@ class Config: atomic_writes=True, ) - if not (data := await store.async_load()) or not isinstance(data, dict): + if not (data := await store.async_load()): return # In 2021.9 we fixed validation to disallow a path (because that's never correct) @@ -1998,7 +1998,7 @@ class Config: "currency": self.currency, } - store = Store( + store: Store[dict[str, Any]] = Store( self.hass, CORE_STORAGE_VERSION, CORE_STORAGE_KEY, diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py new file mode 100644 index 00000000000..ef8193dad28 --- /dev/null +++ b/homeassistant/generated/bluetooth.py @@ -0,0 +1,97 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +""" +from __future__ import annotations + +# fmt: off + +BLUETOOTH: list[dict[str, str | int | list[int]]] = [ + { + "domain": "fjaraskupan", + "manufacturer_id": 20296, + "manufacturer_data_start": [ + 79, + 68, + 70, + 74, + 65, + 82 + ] + }, + { + "domain": "govee_ble", + "local_name": "Govee*" + }, + { + "domain": "govee_ble", + "local_name": "GVH5*" + }, + { + "domain": "govee_ble", + "local_name": "B5178*" + }, + { + "domain": "govee_ble", + "manufacturer_id": 26589, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "govee_ble", + "manufacturer_id": 18994, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "govee_ble", + "manufacturer_id": 14474, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "govee_ble", + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "homekit_controller", + "manufacturer_id": 76, + "manufacturer_data_start": [ + 6 + ] + }, + { + "domain": "inkbird", + "local_name": "sps" + }, + { + "domain": "inkbird", + "local_name": "Inkbird*" + }, + { + "domain": "inkbird", + "local_name": "iBBQ*" + }, + { + "domain": "inkbird", + "local_name": "tps" + }, + { + "domain": "moat", + "local_name": "Moat_S*" + }, + { + "domain": "sensorpush", + "local_name": "SensorPush*" + }, + { + "domain": "switchbot", + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "switchbot", + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + }, + { + "domain": "xiaomi_ble", + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + } +] diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d7ed9159d7f..28d0ad6b44b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -29,6 +29,7 @@ FLOWS = { "ambiclimate", "ambient_station", "androidtv", + "anthemav", "apple_tv", "arcam_fmj", "aseko_pool_live", @@ -46,6 +47,7 @@ FLOWS = { "balboa", "blebox", "blink", + "bluetooth", "bmw_connected_drive", "bond", "bosch_shc", @@ -136,6 +138,7 @@ FLOWS = { "goodwe", "google", "google_travel_time", + "govee_ble", "gpslogger", "gree", "growatt_server", @@ -165,6 +168,7 @@ FLOWS = { "iaqualink", "icloud", "ifttt", + "inkbird", "insteon", "intellifire", "ios", @@ -214,6 +218,7 @@ FLOWS = { "mill", "minecraft_server", "mjpeg", + "moat", "mobile_app", "modem_callerid", "modern_forms", @@ -234,6 +239,7 @@ FLOWS = { "netatmo", "netgear", "nexia", + "nextdns", "nfandroidtv", "nightscout", "nina", @@ -288,6 +294,7 @@ FLOWS = { "recollect_waste", "renault", "rfxtrx", + "rhasspy", "ridwell", "ring", "risco", @@ -305,6 +312,7 @@ FLOWS = { "sense", "senseme", "sensibo", + "sensorpush", "sentry", "senz", "sharkiq", @@ -332,6 +340,7 @@ FLOWS = { "sonarr", "songpal", "sonos", + "soundtouch", "speedtestdotnet", "spider", "spotify", @@ -413,6 +422,7 @@ FLOWS = { "ws66i", "xbox", "xiaomi_aqara", + "xiaomi_ble", "xiaomi_miio", "yale_smart_alarm", "yamaha_musiccast", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 71c8dd530c2..fb8000f8393 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -58,6 +58,8 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'isy994', 'registered_devices': True}, {'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'}, {'domain': 'isy994', 'hostname': 'polisy*', 'macaddress': '000DB9*'}, + {'domain': 'lifx', 'macaddress': 'D073D5*'}, + {'domain': 'lifx', 'registered_devices': True}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': 'B82CA0*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '00D02D*'}, @@ -127,6 +129,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'tolo', 'hostname': 'usr-tcp232-ed2'}, {'domain': 'toon', 'hostname': 'eneco-*', 'macaddress': '74C63B*'}, {'domain': 'tplink', 'registered_devices': True}, + {'domain': 'tplink', 'hostname': 'es*', 'macaddress': '54AF97*'}, {'domain': 'tplink', 'hostname': 'ep*', 'macaddress': 'E848B8*'}, {'domain': 'tplink', 'hostname': 'ep*', 'macaddress': '003192*'}, {'domain': 'tplink', 'hostname': 'hs*', 'macaddress': '1C3BF3*'}, diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py new file mode 100644 index 00000000000..4e151f5578d --- /dev/null +++ b/homeassistant/generated/supported_brands.py @@ -0,0 +1,16 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +""" + +# fmt: off + +HAS_SUPPORTED_BRANDS = ( + "denonavr", + "hunterdouglas_powerview", + "motion_blinds", + "overkiz", + "renault", + "wemo", + "zwave_js" +) diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index faca1c17854..5284eef02a7 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -167,6 +167,11 @@ ZEROCONF = { "name": "*z.wave-me*" } ], + "_hap._udp.local.": [ + { + "domain": "homekit_controller" + } + ], "_homekit._tcp.local.": [ { "domain": "homekit" @@ -347,6 +352,11 @@ ZEROCONF = { "domain": "sonos" } ], + "_soundtouch._tcp.local.": [ + { + "domain": "soundtouch" + } + ], "_spotify-connect._tcp.local.": [ { "domain": "spotify" diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2ef96091d15..f44b59ff077 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -7,7 +7,7 @@ from contextlib import suppress from ssl import SSLContext import sys from types import MappingProxyType -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import aiohttp from aiohttp import web @@ -22,7 +22,11 @@ from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util from .frame import warn_use -from .json import json_dumps +from .json import json_dumps, json_loads + +if TYPE_CHECKING: + from aiohttp.typedefs import JSONDecoder + DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" @@ -35,6 +39,19 @@ SERVER_SOFTWARE = "HomeAssistant/{0} aiohttp/{1} Python/{2[0]}.{2[1]}".format( WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session" +class HassClientResponse(aiohttp.ClientResponse): + """aiohttp.ClientResponse with a json method that uses json_loads by default.""" + + async def json( + self, + *args: Any, + loads: JSONDecoder = json_loads, + **kwargs: Any, + ) -> Any: + """Send a json request and parse the json response.""" + return await super().json(*args, loads=loads, **kwargs) + + @callback @bind_hass def async_get_clientsession( @@ -99,6 +116,7 @@ def _async_create_clientsession( clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), json_serialize=json_dumps, + response_class=HassClientResponse, **kwargs, ) # Prevent packages accidentally overriding our default headers diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index e5d35ccbf44..aeb52e8faed 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Container, Iterable, MutableMapping -from typing import cast +from typing import Optional, cast import attr @@ -49,7 +49,9 @@ class AreaRegistry: """Initialize the area registry.""" self.hass = hass self.areas: MutableMapping[str, AreaEntry] = {} - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) + self._store = Store[dict[str, list[dict[str, Optional[str]]]]]( + hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) self._normalized_name_area_idx: dict[str, str] = {} @callback @@ -176,8 +178,9 @@ class AreaRegistry: areas: MutableMapping[str, AreaEntry] = OrderedDict() - if isinstance(data, dict): + if data is not None: for area in data["areas"]: + assert area["name"] is not None and area["id"] is not None normalized_name = normalize_area_name(area["name"]) areas[area["id"]] = AreaEntry( name=area["name"], diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 3617c0b1f29..c9d6ffe6065 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,17 +6,21 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import dhcp, onboarding, ssdp, zeroconf +from homeassistant.components import onboarding from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType +from .typing import DiscoveryInfoType if TYPE_CHECKING: import asyncio - from homeassistant.components import mqtt + from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + from homeassistant.components.dhcp import DhcpServiceInfo + from homeassistant.components.ssdp import SsdpServiceInfo + from homeassistant.components.zeroconf import ZeroconfServiceInfo + from .service_info.mqtt import MqttServiceInfo _R = TypeVar("_R", bound="Awaitable[bool] | bool") DiscoveryFunctionType = Callable[[HomeAssistant], _R] @@ -92,7 +96,18 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle a flow initialized by bluetooth discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(self._domain) + + return await self.async_step_confirm() + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: """Handle a flow initialized by dhcp discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -102,7 +117,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_homekit( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by Homekit discovery.""" if self._async_in_progress() or self._async_current_entries(): @@ -112,7 +127,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_mqtt(self, discovery_info: mqtt.MqttServiceInfo) -> FlowResult: + async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: """Handle a flow initialized by mqtt discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -122,7 +137,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_zeroconf( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by Zeroconf discovery.""" if self._async_in_progress() or self._async_current_entries(): @@ -132,7 +147,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp(self, discovery_info: SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by Ssdp discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -158,23 +173,8 @@ def register_discovery_flow( domain: str, title: str, discovery_function: DiscoveryFunctionType[Awaitable[bool] | bool], - connection_class: str | UndefinedType = UNDEFINED, ) -> None: """Register flow for discovered integrations that not require auth.""" - if connection_class is not UNDEFINED: - _LOGGER.warning( - ( - "The %s (%s) integration is setting a connection_class" - " when calling the 'register_discovery_flow()' method in its" - " config flow. The connection class has been deprecated and will" - " be removed in a future release of Home Assistant." - " If '%s' is a custom integration, please contact the author" - " of that integration about this warning.", - ), - title, - domain, - domain, - ) class DiscoveryFlow(DiscoveryFlowHandler[Union[Awaitable[bool], bool]]): """Discovery flow handler.""" diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 444876a7674..428a62f0c9d 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -102,7 +102,7 @@ class FlowManagerResourceView(_BaseFlowManagerView): @RequestDataValidator(vol.Schema(dict), allow_empty=True) async def post( - self, request: web.Request, flow_id: str, data: dict[str, Any] + self, request: web.Request, data: dict[str, Any], flow_id: str ) -> web.Response: """Handle a POST request.""" try: diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 7937459b50c..2fbdefd7ec0 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -2,14 +2,16 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Callable from logging import Logger -from typing import Any +from typing import Generic, TypeVar from homeassistant.core import HassJob, HomeAssistant, callback +_R_co = TypeVar("_R_co", covariant=True) -class Debouncer: + +class Debouncer(Generic[_R_co]): """Class to rate limit calls to a specific command.""" def __init__( @@ -19,7 +21,7 @@ class Debouncer: *, cooldown: float, immediate: bool, - function: Callable[..., Awaitable[Any]] | None = None, + function: Callable[[], _R_co] | None = None, ) -> None: """Initialize debounce. @@ -35,15 +37,17 @@ class Debouncer: self._timer_task: asyncio.TimerHandle | None = None self._execute_at_end_of_timer: bool = False self._execute_lock = asyncio.Lock() - self._job: HassJob | None = None if function is None else HassJob(function) + self._job: HassJob[[], _R_co] | None = ( + None if function is None else HassJob(function) + ) @property - def function(self) -> Callable[..., Awaitable[Any]] | None: + def function(self) -> Callable[[], _R_co] | None: """Return the function being wrapped by the Debouncer.""" return self._function @function.setter - def function(self, function: Callable[..., Awaitable[Any]]) -> None: + def function(self, function: Callable[[], _R_co]) -> None: """Update the function being wrapped by the Debouncer.""" self._function = function if self._job is None or function != self._job.target: diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 4bf57c1a4e1..8d961d7008b 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -5,12 +5,20 @@ from collections.abc import Callable import functools import inspect import logging -from typing import Any +from typing import Any, TypeVar + +from typing_extensions import ParamSpec from ..helpers.frame import MissingIntegrationFrame, get_integration_frame +_ObjectT = TypeVar("_ObjectT", bound=object) +_R = TypeVar("_R") +_P = ParamSpec("_P") -def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: + +def deprecated_substitute( + substitute_name: str, +) -> Callable[[Callable[[_ObjectT], Any]], Callable[[_ObjectT], Any]]: """Help migrate properties to new names. When a property is added to replace an older property, this decorator can @@ -19,10 +27,10 @@ def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: warning will be issued alerting the user of the impending change. """ - def decorator(func: Callable) -> Callable: + def decorator(func: Callable[[_ObjectT], Any]) -> Callable[[_ObjectT], Any]: """Decorate function as deprecated.""" - def func_wrapper(self: Callable) -> Any: + def func_wrapper(self: _ObjectT) -> Any: """Wrap for the original function.""" if hasattr(self, substitute_name): # If this platform is still using the old property, issue @@ -81,14 +89,16 @@ def get_deprecated( return config.get(new_name, default) -def deprecated_class(replacement: str) -> Any: +def deprecated_class( + replacement: str, +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: """Mark class as deprecated and provide a replacement class to be used instead.""" - def deprecated_decorator(cls: Any) -> Any: + def deprecated_decorator(cls: Callable[_P, _R]) -> Callable[_P, _R]: """Decorate class as deprecated.""" @functools.wraps(cls) - def deprecated_cls(*args: Any, **kwargs: Any) -> Any: + def deprecated_cls(*args: _P.args, **kwargs: _P.kwargs) -> _R: """Wrap for the original class.""" _print_deprecation_warning(cls, replacement, "class") return cls(*args, **kwargs) @@ -98,14 +108,16 @@ def deprecated_class(replacement: str) -> Any: return deprecated_decorator -def deprecated_function(replacement: str) -> Callable[..., Callable]: +def deprecated_function( + replacement: str, +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: """Mark function as deprecated and provide a replacement function to be used instead.""" - def deprecated_decorator(func: Callable) -> Callable: + def deprecated_decorator(func: Callable[_P, _R]) -> Callable[_P, _R]: """Decorate function as deprecated.""" @functools.wraps(func) - def deprecated_func(*args: Any, **kwargs: Any) -> Any: + def deprecated_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: """Wrap for the original function.""" _print_deprecation_warning(func, replacement, "function") return func(*args, **kwargs) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index ed3d5a7b06f..f133eba8f92 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Coroutine import logging import time from typing import TYPE_CHECKING, Any, NamedTuple, cast @@ -37,6 +38,7 @@ STORAGE_VERSION_MINOR = 3 SAVE_DELAY = 10 CLEANUP_DELAY = 10 +CONNECTION_BLUETOOTH = "bluetooth" CONNECTION_NETWORK_MAC = "mac" CONNECTION_UPNP = "upnp" CONNECTION_ZIGBEE = "zigbee" @@ -164,7 +166,7 @@ def _async_get_device_id_from_index( return None -class DeviceRegistryStore(storage.Store): +class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): """Store entity registry data.""" async def _async_migrate_func( @@ -569,7 +571,6 @@ class DeviceRegistry: deleted_devices = OrderedDict() if data is not None: - data = cast("dict[str, Any]", data) for device in data["devices"]: devices[device["id"]] = DeviceEntry( area_id=device["area_id"], @@ -833,7 +834,7 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: ent_reg = entity_registry.async_get(hass) async_cleanup(hass, dev_reg, ent_reg) - debounced_cleanup = Debouncer( + debounced_cleanup: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, _LOGGER, cooldown=CLEANUP_DELAY, immediate=False, function=cleanup ) diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index d1f7b2b97f9..c7ad4fb1adf 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -1,7 +1,7 @@ """Helpers for Home Assistant dispatcher & internal component/platform.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine import logging from typing import Any @@ -41,26 +41,13 @@ def async_dispatcher_connect( """ if DATA_DISPATCHER not in hass.data: hass.data[DATA_DISPATCHER] = {} - - job = HassJob( - catch_log_exception( - target, - lambda *args: "Exception in {} when dispatching '{}': {}".format( - # Functions wrapped in partial do not have a __name__ - getattr(target, "__name__", None) or str(target), - signal, - args, - ), - ) - ) - - hass.data[DATA_DISPATCHER].setdefault(signal, []).append(job) + hass.data[DATA_DISPATCHER].setdefault(signal, {})[target] = None @callback def async_remove_dispatcher() -> None: """Remove signal listener.""" try: - hass.data[DATA_DISPATCHER][signal].remove(job) + del hass.data[DATA_DISPATCHER][signal][target] except (KeyError, ValueError): # KeyError is key target listener did not exist # ValueError if listener did not exist within signal @@ -75,6 +62,23 @@ def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args) +def _generate_job( + signal: str, target: Callable[..., Any] +) -> HassJob[..., None | Coroutine[Any, Any, None]]: + """Generate a HassJob for a signal and target.""" + return HassJob( + catch_log_exception( + target, + lambda *args: "Exception in {} when dispatching '{}': {}".format( + # Functions wrapped in partial do not have a __name__ + getattr(target, "__name__", None) or str(target), + signal, + args, + ), + ) + ) + + @callback @bind_hass def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: @@ -82,7 +86,21 @@ def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: This method must be run in the event loop. """ - target_list = hass.data.get(DATA_DISPATCHER, {}).get(signal, []) + target_list: dict[ + Callable[..., Any], HassJob[..., None | Coroutine[Any, Any, None]] | None + ] = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) - for job in target_list: - hass.async_add_hass_job(job, *args) + run: list[HassJob[..., None | Coroutine[Any, Any, None]]] = [] + for target, job in target_list.items(): + if job is None: + job = _generate_job(signal, target) + target_list[target] = job + + # Run the jobs all at the end + # to ensure no jobs add more dispatchers + # which can result in the target_list + # changing size during iteration + run.append(job) + + for job in run: + hass.async_run_hass_job(job, *args) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f00f7d85e76..cb71cfd9edf 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -936,7 +936,7 @@ class Entity(ABC): """Suggest to report an issue.""" report_issue = "" if "custom_components" in type(self).__module__: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6253b939bed..c5c61cc1b0d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -2,11 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import Awaitable, Callable, Coroutine, Iterable from contextvars import ContextVar from datetime import datetime, timedelta from logging import Logger, getLogger -from types import ModuleType from typing import TYPE_CHECKING, Any, Protocol from urllib.parse import urlparse @@ -71,6 +70,36 @@ class AddEntitiesCallback(Protocol): """Define add_entities type.""" +class EntityPlatformModule(Protocol): + """Protocol type for entity platform modules.""" + + async def async_setup_platform( + self, + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up an integration platform async.""" + + def setup_platform( + self, + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up an integration platform.""" + + async def async_setup_entry( + self, + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up an integration platform from a config entry.""" + + class EntityPlatform: """Manage the entities for a single platform.""" @@ -81,7 +110,7 @@ class EntityPlatform: logger: Logger, domain: str, platform_name: str, - platform: ModuleType | None, + platform: EntityPlatformModule | None, scan_interval: timedelta, entity_namespace: str | None, ) -> None: @@ -95,7 +124,7 @@ class EntityPlatform: self.entity_namespace = entity_namespace self.config_entry: config_entries.ConfigEntry | None = None self.entities: dict[str, Entity] = {} - self._tasks: list[asyncio.Future] = [] + self._tasks: list[asyncio.Task[None]] = [] # Stop tracking tasks after setup is completed self._setup_complete = False # Method to cancel the state change listener @@ -169,10 +198,12 @@ class EntityPlatform: return @callback - def async_create_setup_task() -> Coroutine: + def async_create_setup_task() -> Coroutine[ + Any, Any, None + ] | asyncio.Future[None]: """Get task to set up platform.""" if getattr(platform, "async_setup_platform", None): - return platform.async_setup_platform( # type: ignore[no-any-return,union-attr] + return platform.async_setup_platform( # type: ignore[union-attr] hass, platform_config, self._async_schedule_add_entities, @@ -181,7 +212,7 @@ class EntityPlatform: # This should not be replaced with hass.async_add_job because # we don't want to track this task in case it blocks startup. - return hass.loop.run_in_executor( # type: ignore[return-value] + return hass.loop.run_in_executor( None, platform.setup_platform, # type: ignore[union-attr] hass, @@ -211,18 +242,18 @@ class EntityPlatform: platform = self.platform @callback - def async_create_setup_task() -> Coroutine: + def async_create_setup_task() -> Coroutine[Any, Any, None]: """Get task to set up platform.""" config_entries.current_entry.set(config_entry) - return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] + return platform.async_setup_entry( # type: ignore[union-attr] self.hass, config_entry, self._async_schedule_add_entities_for_entry ) return await self._async_setup_platform(async_create_setup_task) async def _async_setup_platform( - self, async_create_setup_task: Callable[[], Coroutine], tries: int = 0 + self, async_create_setup_task: Callable[[], Awaitable[None]], tries: int = 0 ) -> bool: """Set up a platform via config file or config entry. @@ -544,6 +575,7 @@ class EntityPlatform: entity_category=entity.entity_category, hidden_by=hidden_by, known_object_ids=self.entities.keys(), + has_entity_name=entity.has_entity_name, original_device_class=entity.device_class, original_icon=entity.icon, original_name=entity.name, @@ -700,7 +732,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: dict | vol.Schema, + schema: dict[str, Any] | vol.Schema, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, ) -> None: @@ -752,7 +784,7 @@ class EntityPlatform: return async with self._process_updates: - tasks = [] + tasks: list[Coroutine[Any, Any, None]] = [] for entity in self.entities.values(): if not entity.should_poll: continue diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 85cd684fca1..d18af953ec6 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -141,7 +141,7 @@ def threaded_listener_factory( def async_track_state_change( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[str, State, State], Awaitable[None] | None], + action: Callable[[str, State | None, State], Awaitable[None] | None], from_state: None | str | Iterable[str] = None, to_state: None | str | Iterable[str] = None, ) -> CALLBACK_TYPE: @@ -197,9 +197,9 @@ def async_track_state_change( """Handle specific state changes.""" hass.async_run_hass_job( job, - event.data.get("entity_id"), + event.data["entity_id"], event.data.get("old_state"), - event.data.get("new_state"), + event.data["new_state"], ) @callback diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index b81f5f29432..ca5cf759d8e 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -96,7 +96,7 @@ def report_integration( index = found_frame.filename.index(path) if path == "custom_components/": - extra = " to the custom component author" + extra = " to the custom integration author" else: extra = "" diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py index 59a4cf39498..8561d10794c 100644 --- a/homeassistant/helpers/instance_id.py +++ b/homeassistant/helpers/instance_id.py @@ -16,7 +16,7 @@ LEGACY_UUID_FILE = ".uuid" @singleton.singleton(DATA_KEY) async def async_get(hass: HomeAssistant) -> str: """Get unique ID for the hass instance.""" - store = storage.Store(hass, DATA_VERSION, DATA_KEY, True) + store = storage.Store[dict[str, str]](hass, DATA_VERSION, DATA_KEY, True) data: dict[str, str] | None = await storage.async_migrator( hass, diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index a51d9de59e2..2049300e460 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -1,7 +1,8 @@ """Helpers to check recorder.""" +import asyncio -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback def async_migration_in_progress(hass: HomeAssistant) -> bool: @@ -12,3 +13,26 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: from homeassistant.components import recorder return recorder.util.async_migration_in_progress(hass) + + +@callback +def async_initialize_recorder(hass: HomeAssistant) -> None: + """Initialize recorder data.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.recorder import const, models + + hass.data[const.DOMAIN] = models.RecorderData() + + +async def async_wait_recorder(hass: HomeAssistant) -> bool: + """Wait for recorder to initialize and return connection status. + + Returns False immediately if the recorder is not enabled. + """ + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.recorder import const + + if const.DOMAIN not in hass.data: + return False + db_connected: asyncio.Future[bool] = hass.data[const.DOMAIN].db_connected + return await db_connected diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index b8262d3a533..73a00898d69 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -32,7 +32,7 @@ STATE_DUMP_INTERVAL = timedelta(minutes=15) # How long should a saved state be preserved if the entity no longer exists STATE_EXPIRATION = timedelta(days=7) -_StoredStateT = TypeVar("_StoredStateT", bound="StoredState") +_StoredStateSelfT = TypeVar("_StoredStateSelfT", bound="StoredState") class ExtraStoredData: @@ -82,7 +82,7 @@ class StoredState: return result @classmethod - def from_dict(cls: type[_StoredStateT], json_dict: dict) -> _StoredStateT: + def from_dict(cls: type[_StoredStateSelfT], json_dict: dict) -> _StoredStateSelfT: """Initialize a stored state from a dict.""" extra_data_dict = json_dict.get("extra_data") extra_data = RestoredExtraData(extra_data_dict) if extra_data_dict else None @@ -139,7 +139,7 @@ class RestoreStateData: def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" self.hass: HomeAssistant = hass - self.store: Store = Store( + self.store = Store[list[dict[str, Any]]]( hass, STORAGE_VERSION, STORAGE_KEY, encoder=JSONEncoder ) self.last_states: dict[str, StoredState] = {} @@ -305,9 +305,7 @@ class RestoreEntity(Entity): # Return None if this entity isn't added to hass yet _LOGGER.warning("Cannot get last state. Entity not added to hass") # type: ignore[unreachable] return None - data = cast( - RestoreStateData, await RestoreStateData.async_get_instance(self.hass) - ) + data = await RestoreStateData.async_get_instance(self.hass) if self.entity_id not in data.last_states: return None return data.last_states[self.entity_id] diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index bc3451c24c0..cf7fb3b2304 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -31,13 +31,7 @@ from homeassistant.exceptions import ( Unauthorized, UnknownUser, ) -from homeassistant.loader import ( - MAX_LOAD_CONCURRENTLY, - Integration, - async_get_integration, - bind_hass, -) -from homeassistant.util.async_ import gather_with_concurrency +from homeassistant.loader import Integration, async_get_integrations, bind_hass from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml.loader import JSON_TYPE @@ -467,10 +461,12 @@ async def async_get_all_descriptions( loaded = {} if missing: - integrations = await gather_with_concurrency( - MAX_LOAD_CONCURRENTLY, - *(async_get_integration(hass, domain) for domain in missing), - ) + ints_or_excs = await async_get_integrations(hass, missing) + integrations = [ + int_or_exc + for int_or_exc in ints_or_excs.values() + if isinstance(int_or_exc, Integration) + ] contents = await hass.async_add_executor_job( _load_services_files, hass, integrations diff --git a/homeassistant/helpers/service_info/__init__.py b/homeassistant/helpers/service_info/__init__.py new file mode 100644 index 00000000000..b907fd9bbd1 --- /dev/null +++ b/homeassistant/helpers/service_info/__init__.py @@ -0,0 +1 @@ +"""Service info helpers.""" diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py new file mode 100644 index 00000000000..0db3a39b114 --- /dev/null +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -0,0 +1,5 @@ +"""The bluetooth integration service info.""" + +from home_assistant_bluetooth import BluetoothServiceInfo + +__all__ = ["BluetoothServiceInfo"] diff --git a/homeassistant/helpers/service_info/mqtt.py b/homeassistant/helpers/service_info/mqtt.py new file mode 100644 index 00000000000..fcf5d4744f1 --- /dev/null +++ b/homeassistant/helpers/service_info/mqtt.py @@ -0,0 +1,20 @@ +"""MQTT Discovery data.""" +from dataclasses import dataclass +import datetime as dt +from typing import Union + +from homeassistant.data_entry_flow import BaseServiceInfo + +ReceivePayloadType = Union[str, bytes] + + +@dataclass +class MqttServiceInfo(BaseServiceInfo): + """Prepared info from mqtt entries.""" + + topic: str + payload: ReceivePayloadType + qos: int + retain: bool + subscribed_topic: str + timestamp: dt.datetime diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index a4f6d32c303..f15806ae5ff 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -4,23 +4,23 @@ from __future__ import annotations import asyncio from collections.abc import Callable import functools -from typing import TypeVar, cast +from typing import Any, TypeVar, cast from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass _T = TypeVar("_T") -FUNC = Callable[[HomeAssistant], _T] +_FuncType = Callable[[HomeAssistant], _T] -def singleton(data_key: str) -> Callable[[FUNC], FUNC]: +def singleton(data_key: str) -> Callable[[_FuncType[_T]], _FuncType[_T]]: """Decorate a function that should be called once per instance. Result will be cached and simultaneous calls will be handled. """ - def wrapper(func: FUNC) -> FUNC: + def wrapper(func: _FuncType[_T]) -> _FuncType[_T]: """Wrap a function with caching logic.""" if not asyncio.iscoroutinefunction(func): @@ -35,10 +35,10 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: @bind_hass @functools.wraps(func) - async def async_wrapped(hass: HomeAssistant) -> _T: + async def async_wrapped(hass: HomeAssistant) -> Any: if data_key not in hass.data: evt = hass.data[data_key] = asyncio.Event() - result = await func(hass) + result = await func(hass) # type: ignore[misc] hass.data[data_key] = result evt.set() return cast(_T, result) @@ -51,6 +51,6 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: return cast(_T, obj_or_evt) - return async_wrapped + return async_wrapped # type: ignore[return-value] return wrapper diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 554a88f4ad5..6819a1eb48b 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -2,14 +2,14 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Mapping, Sequence from contextlib import suppress from copy import deepcopy import inspect from json import JSONEncoder import logging import os -from typing import Any +from typing import Any, Generic, TypeVar, Union from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback @@ -24,6 +24,8 @@ _LOGGER = logging.getLogger(__name__) STORAGE_SEMAPHORE = "storage_semaphore" +_T = TypeVar("_T", bound=Union[Mapping[str, Any], Sequence[Any]]) + @bind_hass async def async_migrator( @@ -66,7 +68,7 @@ async def async_migrator( @bind_hass -class Store: +class Store(Generic[_T]): """Class to help storing data.""" def __init__( @@ -90,7 +92,7 @@ class Store: self._unsub_delay_listener: CALLBACK_TYPE | None = None self._unsub_final_write_listener: CALLBACK_TYPE | None = None self._write_lock = asyncio.Lock() - self._load_task: asyncio.Future | None = None + self._load_task: asyncio.Future[_T | None] | None = None self._encoder = encoder self._atomic_writes = atomic_writes @@ -99,7 +101,7 @@ class Store: """Return the config path.""" return self.hass.config.path(STORAGE_DIR, self.key) - async def async_load(self) -> dict | list | None: + async def async_load(self) -> _T | None: """Load data. If the expected version and minor version do not match the given versions, the @@ -113,7 +115,7 @@ class Store: return await self._load_task - async def _async_load(self): + async def _async_load(self) -> _T | None: """Load the data and ensure the task is removed.""" if STORAGE_SEMAPHORE not in self.hass.data: self.hass.data[STORAGE_SEMAPHORE] = asyncio.Semaphore(MAX_LOAD_CONCURRENTLY) @@ -178,7 +180,7 @@ class Store: return stored - async def async_save(self, data: dict | list) -> None: + async def async_save(self, data: _T) -> None: """Save data.""" self._data = { "version": self.version, @@ -196,7 +198,7 @@ class Store: @callback def async_delay_save( self, - data_func: Callable[[], dict | list], + data_func: Callable[[], _T], delay: float = 0, ) -> None: """Save data with an optional delay.""" diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index cda50de535b..616baeeea92 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -9,13 +9,11 @@ from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.loader import ( - MAX_LOAD_CONCURRENTLY, Integration, async_get_config_flows, - async_get_integration, + async_get_integrations, bind_hass, ) -from homeassistant.util.async_ import gather_with_concurrency from homeassistant.util.json import load_json _LOGGER = logging.getLogger(__name__) @@ -151,16 +149,13 @@ async def async_get_component_strings( ) -> dict[str, Any]: """Load translations.""" domains = list({loaded.split(".")[-1] for loaded in components}) - integrations = dict( - zip( - domains, - await gather_with_concurrency( - MAX_LOAD_CONCURRENTLY, - *(async_get_integration(hass, domain) for domain in domains), - ), - ) - ) + integrations: dict[str, Integration] = {} + ints_or_excs = await async_get_integrations(hass, domains) + for domain, int_or_exc in ints_or_excs.items(): + if isinstance(int_or_exc, Exception): + raise int_or_exc + integrations[domain] = int_or_exc translations: dict[str, Any] = {} # Determine paths of missing components/platforms diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index fc619469500..768b8040729 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable, Coroutine, Generator from datetime import datetime, timedelta import logging from time import monotonic @@ -44,7 +44,7 @@ class DataUpdateCoordinator(Generic[_T]): name: str, update_interval: timedelta | None = None, update_method: Callable[[], Awaitable[_T]] | None = None, - request_refresh_debouncer: Debouncer | None = None, + request_refresh_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, ) -> None: """Initialize global data updater.""" self.hass = hass @@ -282,6 +282,15 @@ class DataUpdateCoordinator(Generic[_T]): self.async_update_listeners() + @callback + def async_set_update_error(self, err: Exception) -> None: + """Manually set an error, log the message and notify listeners.""" + self.last_exception = err + if self.last_update_success: + self.logger.error("Error requesting %s data: %s", self.name, err) + self.last_update_success = False + self.async_update_listeners() + @callback def async_set_updated_data(self, data: _T) -> None: """Manually update data, notify listeners and reset refresh interval.""" diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ab681d7c42d..e4aff23d3f1 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -7,7 +7,7 @@ documentation as possible to keep it understandable. from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Iterable from contextlib import suppress import functools as ft import importlib @@ -24,13 +24,13 @@ from awesomeversion import ( ) from .generated.application_credentials import APPLICATION_CREDENTIALS +from .generated.bluetooth import BLUETOOTH from .generated.dhcp import DHCP from .generated.mqtt import MQTT from .generated.ssdp import SSDP from .generated.usb import USB from .generated.zeroconf import HOMEKIT, ZEROCONF from .helpers.json import JSON_DECODE_EXCEPTIONS, json_loads -from .util.async_ import gather_with_concurrency # Typing imports that create a circular dependency if TYPE_CHECKING: @@ -77,6 +77,26 @@ class DHCPMatcher(DHCPMatcherRequired, DHCPMatcherOptional): """Matcher for the dhcp integration.""" +class BluetoothMatcherRequired(TypedDict, total=True): + """Matcher for the bluetooth integration for required fields.""" + + domain: str + + +class BluetoothMatcherOptional(TypedDict, total=False): + """Matcher for the bluetooth integration for optional fields.""" + + local_name: str + service_uuid: str + service_data_uuid: str + manufacturer_id: int + manufacturer_data_start: list[int] + + +class BluetoothMatcher(BluetoothMatcherRequired, BluetoothMatcherOptional): + """Matcher for the bluetooth integration.""" + + class Manifest(TypedDict, total=False): """ Integration manifest. @@ -97,6 +117,7 @@ class Manifest(TypedDict, total=False): issue_tracker: str quality_scale: str iot_class: str + bluetooth: list[dict[str, int | str]] mqtt: list[str] ssdp: list[dict[str, str]] zeroconf: list[str | dict[str, str]] @@ -107,6 +128,7 @@ class Manifest(TypedDict, total=False): version: str codeowners: list[str] loggers: list[str] + supported_brands: dict[str, str] def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: @@ -145,19 +167,15 @@ async def _async_get_custom_components( get_sub_directories, custom_components.__path__ ) - integrations = await gather_with_concurrency( - MAX_LOAD_CONCURRENTLY, - *( - hass.async_add_executor_job( - Integration.resolve_from_root, hass, custom_components, comp.name - ) - for comp in dirs - ), + integrations = await hass.async_add_executor_job( + _resolve_integrations_from_root, + hass, + custom_components, + [comp.name for comp in dirs], ) - return { integration.domain: integration - for integration in integrations + for integration in integrations.values() if integration is not None } @@ -269,6 +287,22 @@ async def async_get_zeroconf( return zeroconf +async def async_get_bluetooth(hass: HomeAssistant) -> list[BluetoothMatcher]: + """Return cached list of bluetooth types.""" + bluetooth = cast(list[BluetoothMatcher], BLUETOOTH.copy()) + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if not integration.bluetooth: + continue + for entry in integration.bluetooth: + bluetooth.append( + cast(BluetoothMatcher, {"domain": integration.domain, **entry}) + ) + + return bluetooth + + async def async_get_dhcp(hass: HomeAssistant) -> list[DHCPMatcher]: """Return cached list of dhcp types.""" dhcp = cast(list[DHCPMatcher], DHCP.copy()) @@ -519,6 +553,11 @@ class Integration: """Return Integration zeroconf entries.""" return self.manifest.get("zeroconf") + @property + def bluetooth(self) -> list[dict[str, str | int]] | None: + """Return Integration bluetooth entries.""" + return self.manifest.get("bluetooth") + @property def dhcp(self) -> list[dict[str, str | bool]] | None: """Return Integration dhcp entries.""" @@ -639,59 +678,101 @@ class Integration: return f"" +def _resolve_integrations_from_root( + hass: HomeAssistant, root_module: ModuleType, domains: list[str] +) -> dict[str, Integration]: + """Resolve multiple integrations from root.""" + integrations: dict[str, Integration] = {} + for domain in domains: + try: + integration = Integration.resolve_from_root(hass, root_module, domain) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error loading integration: %s", domain) + else: + if integration: + integrations[domain] = integration + return integrations + + async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration: - """Get an integration.""" + """Get integration.""" + integrations_or_excs = await async_get_integrations(hass, [domain]) + int_or_exc = integrations_or_excs[domain] + if isinstance(int_or_exc, Integration): + return int_or_exc + raise int_or_exc + + +async def async_get_integrations( + hass: HomeAssistant, domains: Iterable[str] +) -> dict[str, Integration | Exception]: + """Get integrations.""" if (cache := hass.data.get(DATA_INTEGRATIONS)) is None: if not _async_mount_config_dir(hass): - raise IntegrationNotFound(domain) + return {domain: IntegrationNotFound(domain) for domain in domains} cache = hass.data[DATA_INTEGRATIONS] = {} - int_or_evt: Integration | asyncio.Event | None = cache.get(domain, _UNDEF) + results: dict[str, Integration | Exception] = {} + needed: dict[str, asyncio.Event] = {} + in_progress: dict[str, asyncio.Event] = {} + for domain in domains: + int_or_evt: Integration | asyncio.Event | None = cache.get(domain, _UNDEF) + if isinstance(int_or_evt, asyncio.Event): + in_progress[domain] = int_or_evt + elif int_or_evt is not _UNDEF: + results[domain] = cast(Integration, int_or_evt) + elif "." in domain: + results[domain] = ValueError(f"Invalid domain {domain}") + else: + needed[domain] = cache[domain] = asyncio.Event() - if isinstance(int_or_evt, asyncio.Event): - await int_or_evt.wait() + if in_progress: + await asyncio.gather(*[event.wait() for event in in_progress.values()]) + for domain in in_progress: + # When we have waited and it's _UNDEF, it doesn't exist + # We don't cache that it doesn't exist, or else people can't fix it + # and then restart, because their config will never be valid. + if (int_or_evt := cache.get(domain, _UNDEF)) is _UNDEF: + results[domain] = IntegrationNotFound(domain) + else: + results[domain] = cast(Integration, int_or_evt) - # When we have waited and it's _UNDEF, it doesn't exist - # We don't cache that it doesn't exist, or else people can't fix it - # and then restart, because their config will never be valid. - if (int_or_evt := cache.get(domain, _UNDEF)) is _UNDEF: - raise IntegrationNotFound(domain) + # First we look for custom components + if needed: + # Instead of using resolve_from_root we use the cache of custom + # components to find the integration. + custom = await async_get_custom_components(hass) + for domain, event in needed.items(): + if integration := custom.get(domain): + results[domain] = cache[domain] = integration + event.set() - if int_or_evt is not _UNDEF: - return cast(Integration, int_or_evt) + for domain in results: + if domain in needed: + del needed[domain] - event = cache[domain] = asyncio.Event() + # Now the rest use resolve_from_root + if needed: + from . import components # pylint: disable=import-outside-toplevel - try: - integration = await _async_get_integration(hass, domain) - except Exception: - # Remove event from cache. - cache.pop(domain) - event.set() - raise + integrations = await hass.async_add_executor_job( + _resolve_integrations_from_root, hass, components, list(needed) + ) + for domain, event in needed.items(): + int_or_exc = integrations.get(domain) + if not int_or_exc: + cache.pop(domain) + results[domain] = IntegrationNotFound(domain) + elif isinstance(int_or_exc, Exception): + cache.pop(domain) + exc = IntegrationNotFound(domain) + exc.__cause__ = int_or_exc + results[domain] = exc + else: + results[domain] = cache[domain] = int_or_exc + event.set() - cache[domain] = integration - event.set() - return integration - - -async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integration: - if "." in domain: - raise ValueError(f"Invalid domain {domain}") - - # Instead of using resolve_from_root we use the cache of custom - # components to find the integration. - if integration := (await async_get_custom_components(hass)).get(domain): - return integration - - from . import components # pylint: disable=import-outside-toplevel - - if integration := await hass.async_add_executor_job( - Integration.resolve_from_root, hass, components, domain - ): - return integration - - raise IntegrationNotFound(domain) + return results class LoaderError(Exception): diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d27d9382c37..f2140a9eca7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,23 +10,26 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 +bleak==0.15.0 +bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 -home-assistant-frontend==20220707.1 +home-assistant-bluetooth==1.3.0 +home-assistant-frontend==20220802.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 -lru-dict==1.1.7 -orjson==3.7.5 +lru-dict==1.1.8 +orjson==3.7.8 paho-mqtt==1.6.1 -pillow==9.1.1 -pip>=21.0,<22.2 +pillow==9.2.0 +pip>=21.0,<22.3 pyserial==3.5 python-slugify==4.0.1 -pyudev==0.22.0 +pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 scapy==2.4.5 @@ -88,7 +91,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.0 +numpy==1.23.1 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 2599b4b3c85..8d5f9e52025 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -265,6 +265,10 @@ async def _async_setup_component( await asyncio.sleep(0) await hass.config_entries.flow.async_wait_init_flow_finish(domain) + # Add to components before the async_setup + # call to avoid a deadlock when forwarding platforms + hass.config.components.add(domain) + await asyncio.gather( *( entry.async_setup(hass, integration=integration) @@ -272,8 +276,6 @@ async def _async_setup_component( ) ) - hass.config.components.add(domain) - # Cleanup if domain in hass.data[DATA_SETUP]: hass.data[DATA_SETUP].pop(domain) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 8b96e85664d..e9c5a41062e 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -150,7 +150,7 @@ def check_loop( integration = found_frame.filename[start:end] if path == "custom_components/": - extra = " to the custom component author" + extra = " to the custom integration author" else: extra = "" diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 448b417cc97..494ee04546c 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -294,22 +294,20 @@ def color_xy_brightness_to_RGB( b = X * 0.051713 - Y * 0.121364 + Z * 1.011530 # Apply reverse gamma correction. - r, g, b = map( - lambda x: (12.92 * x) # type: ignore[no-any-return] - if (x <= 0.0031308) - else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055), - [r, g, b], + r, g, b = ( + 12.92 * x if (x <= 0.0031308) else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055) + for x in (r, g, b) ) # Bring all negative components to zero. - r, g, b = map(lambda x: max(0, x), [r, g, b]) + r, g, b = (max(0, x) for x in (r, g, b)) # If one component is greater than 1, weight components by that value. max_component = max(r, g, b) if max_component > 1: - r, g, b = map(lambda x: x / max_component, [r, g, b]) + r, g, b = (x / max_component for x in (r, g, b)) - ir, ig, ib = map(lambda x: int(x * 255), [r, g, b]) + ir, ig, ib = (int(x * 255) for x in (r, g, b)) return (ir, ig, ib) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 18ab43967ec..49ab3c10f8c 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from functools import cache from importlib.metadata import PackageNotFoundError, version import logging import os @@ -23,6 +24,7 @@ def is_virtual_env() -> bool: ) +@cache def is_docker_env() -> bool: """Return True if we run in a docker env.""" return Path("/.dockerenv").exists() diff --git a/mypy.ini b/mypy.ini index 2b657041865..37765023f74 100644 --- a/mypy.ini +++ b/mypy.ini @@ -57,12 +57,24 @@ disallow_any_generics = true [mypy-homeassistant.helpers.condition] disallow_any_generics = true +[mypy-homeassistant.helpers.debounce] +disallow_any_generics = true + +[mypy-homeassistant.helpers.deprecation] +disallow_any_generics = true + [mypy-homeassistant.helpers.discovery] disallow_any_generics = true +[mypy-homeassistant.helpers.dispatcher] +disallow_any_generics = true + [mypy-homeassistant.helpers.entity] disallow_any_generics = true +[mypy-homeassistant.helpers.entity_platform] +disallow_any_generics = true + [mypy-homeassistant.helpers.entity_values] disallow_any_generics = true @@ -75,6 +87,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.script_variables] disallow_any_generics = true +[mypy-homeassistant.helpers.singleton] +disallow_any_generics = true + [mypy-homeassistant.helpers.sun] disallow_any_generics = true @@ -390,6 +405,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.bluetooth.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.bluetooth_tracker.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -962,6 +988,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homeassistant_alerts.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homekit] check_untyped_defs = true disallow_incomplete_defs = true @@ -1358,6 +1395,28 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lifx.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.litterrobot.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.local_ip.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1446,6 +1505,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.metoffice.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1853,6 +1923,28 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.repairs.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.rhasspy.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ridwell.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2612,249 +2704,6 @@ no_implicit_optional = false warn_return_any = false warn_unreachable = false -[mypy-homeassistant.components.blueprint.importer] -ignore_errors = true - -[mypy-homeassistant.components.blueprint.models] -ignore_errors = true - -[mypy-homeassistant.components.blueprint.websocket_api] -ignore_errors = true - -[mypy-homeassistant.components.cloud.client] -ignore_errors = true - -[mypy-homeassistant.components.cloud.http_api] -ignore_errors = true - -[mypy-homeassistant.components.conversation] -ignore_errors = true - -[mypy-homeassistant.components.conversation.default_agent] -ignore_errors = true - -[mypy-homeassistant.components.denonavr.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.denonavr.media_player] -ignore_errors = true - -[mypy-homeassistant.components.denonavr.receiver] -ignore_errors = true - -[mypy-homeassistant.components.evohome] -ignore_errors = true - -[mypy-homeassistant.components.evohome.climate] -ignore_errors = true - -[mypy-homeassistant.components.evohome.water_heater] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.helpers] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.http] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.report_state] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.trait] -ignore_errors = true - -[mypy-homeassistant.components.gree.climate] -ignore_errors = true - -[mypy-homeassistant.components.gree.switch] -ignore_errors = true - -[mypy-homeassistant.components.harmony] -ignore_errors = true - -[mypy-homeassistant.components.harmony.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.harmony.data] -ignore_errors = true - -[mypy-homeassistant.components.hassio] -ignore_errors = true - -[mypy-homeassistant.components.hassio.auth] -ignore_errors = true - -[mypy-homeassistant.components.hassio.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.hassio.ingress] -ignore_errors = true - -[mypy-homeassistant.components.hassio.sensor] -ignore_errors = true - -[mypy-homeassistant.components.hassio.system_health] -ignore_errors = true - -[mypy-homeassistant.components.hassio.websocket_api] -ignore_errors = true - -[mypy-homeassistant.components.home_plus_control] -ignore_errors = true - -[mypy-homeassistant.components.home_plus_control.api] -ignore_errors = true - -[mypy-homeassistant.components.icloud] -ignore_errors = true - -[mypy-homeassistant.components.icloud.account] -ignore_errors = true - -[mypy-homeassistant.components.icloud.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.icloud.sensor] -ignore_errors = true - -[mypy-homeassistant.components.influxdb] -ignore_errors = true - -[mypy-homeassistant.components.input_datetime] -ignore_errors = true - -[mypy-homeassistant.components.izone.climate] -ignore_errors = true - -[mypy-homeassistant.components.konnected] -ignore_errors = true - -[mypy-homeassistant.components.konnected.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.helper] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.select] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.sensor] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.switch] -ignore_errors = true - -[mypy-homeassistant.components.lovelace] -ignore_errors = true - -[mypy-homeassistant.components.lovelace.dashboard] -ignore_errors = true - -[mypy-homeassistant.components.lovelace.resources] -ignore_errors = true - -[mypy-homeassistant.components.lovelace.websocket] -ignore_errors = true - -[mypy-homeassistant.components.lyric.climate] -ignore_errors = true - -[mypy-homeassistant.components.lyric.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.lyric.sensor] -ignore_errors = true - -[mypy-homeassistant.components.melcloud] -ignore_errors = true - -[mypy-homeassistant.components.melcloud.climate] -ignore_errors = true - -[mypy-homeassistant.components.meteo_france.sensor] -ignore_errors = true - -[mypy-homeassistant.components.meteo_france.weather] -ignore_errors = true - -[mypy-homeassistant.components.minecraft_server] -ignore_errors = true - -[mypy-homeassistant.components.minecraft_server.helpers] -ignore_errors = true - -[mypy-homeassistant.components.minecraft_server.sensor] -ignore_errors = true - -[mypy-homeassistant.components.nilu.air_quality] -ignore_errors = true - -[mypy-homeassistant.components.nzbget] -ignore_errors = true - -[mypy-homeassistant.components.nzbget.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.nzbget.coordinator] -ignore_errors = true - -[mypy-homeassistant.components.nzbget.switch] -ignore_errors = true - -[mypy-homeassistant.components.omnilogic.common] -ignore_errors = true - -[mypy-homeassistant.components.omnilogic.sensor] -ignore_errors = true - -[mypy-homeassistant.components.omnilogic.switch] -ignore_errors = true - -[mypy-homeassistant.components.onvif.base] -ignore_errors = true - -[mypy-homeassistant.components.onvif.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.onvif.camera] -ignore_errors = true - -[mypy-homeassistant.components.onvif.device] -ignore_errors = true - -[mypy-homeassistant.components.onvif.sensor] -ignore_errors = true - -[mypy-homeassistant.components.philips_js] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.device_trigger] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.light] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.media_player] -ignore_errors = true - -[mypy-homeassistant.components.plex.media_player] -ignore_errors = true - -[mypy-homeassistant.components.profiler] -ignore_errors = true - -[mypy-homeassistant.components.solaredge.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.solaredge.coordinator] -ignore_errors = true - -[mypy-homeassistant.components.solaredge.sensor] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true @@ -2890,99 +2739,3 @@ ignore_errors = true [mypy-homeassistant.components.sonos.statistics] ignore_errors = true - -[mypy-homeassistant.components.system_health] -ignore_errors = true - -[mypy-homeassistant.components.telegram_bot.polling] -ignore_errors = true - -[mypy-homeassistant.components.template.number] -ignore_errors = true - -[mypy-homeassistant.components.template.sensor] -ignore_errors = true - -[mypy-homeassistant.components.toon] -ignore_errors = true - -[mypy-homeassistant.components.toon.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.toon.models] -ignore_errors = true - -[mypy-homeassistant.components.unifi] -ignore_errors = true - -[mypy-homeassistant.components.unifi.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.unifi.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.unifi.diagnostics] -ignore_errors = true - -[mypy-homeassistant.components.unifi.unifi_entity_base] -ignore_errors = true - -[mypy-homeassistant.components.withings] -ignore_errors = true - -[mypy-homeassistant.components.withings.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.withings.common] -ignore_errors = true - -[mypy-homeassistant.components.withings.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.xbox] -ignore_errors = true - -[mypy-homeassistant.components.xbox.base_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xbox.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xbox.browse_media] -ignore_errors = true - -[mypy-homeassistant.components.xbox.media_source] -ignore_errors = true - -[mypy-homeassistant.components.xbox.sensor] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.air_quality] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.device] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.fan] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.humidifier] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.light] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.sensor] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.switch] -ignore_errors = true diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 5f4f641cbae..680d25414aa 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -53,13 +53,18 @@ class ClassTypeHintMatch: _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), - # x_of_y matches items such as "Awaitable[None]" - "x_of_y": re.compile(r"^(\w+)\[(.*?]*)\]$"), - # x_of_y_comma_z matches items such as "Callable[..., Awaitable[None]]" - "x_of_y_comma_z": re.compile(r"^(\w+)\[(.*?]*), (.*?]*)\]$"), - # x_of_y_of_z_comma_a matches items such as "list[dict[str, Any]]" - "x_of_y_of_z_comma_a": re.compile(r"^(\w+)\[(\w+)\[(.*?]*), (.*?]*)\]\]$"), } +_INNER_MATCH = r"((?:\w+)|(?:\.{3})|(?:\w+\[.+\]))" +_INNER_MATCH_POSSIBILITIES = [i + 1 for i in range(5)] +_TYPE_HINT_MATCHERS.update( + { + f"x_of_y_{i}": re.compile( + rf"^(\w+)\[{_INNER_MATCH}" + f", {_INNER_MATCH}" * (i - 1) + r"\]$" + ) + for i in _INNER_MATCH_POSSIBILITIES + } +) + _MODULE_REGEX: re.Pattern[str] = re.compile(r"^homeassistant\.components\.\w+(\.\w+)?$") @@ -591,6 +596,7 @@ _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ ), ] _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { + # "air_quality": [], # ignored as deprecated "alarm_control_panel": [ ClassTypeHintMatch( base_class="Entity", @@ -717,6 +723,317 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "calendar": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="CalendarEntity", + matches=[ + TypeHintMatch( + function_name="event", + return_type=["CalendarEvent", None], + ), + TypeHintMatch( + function_name="async_get_events", + arg_types={ + 1: "HomeAssistant", + 2: "datetime", + 3: "datetime", + }, + return_type="list[CalendarEvent]", + ), + ], + ), + ], + "camera": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="Camera", + matches=[ + TypeHintMatch( + function_name="entity_picture", + return_type="str", + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="is_recording", + return_type="bool", + ), + TypeHintMatch( + function_name="is_streaming", + return_type="bool", + ), + TypeHintMatch( + function_name="brand", + return_type=["str", None], + ), + TypeHintMatch( + function_name="motion_detection_enabled", + return_type="bool", + ), + TypeHintMatch( + function_name="model", + return_type=["str", None], + ), + TypeHintMatch( + function_name="frame_interval", + return_type="float", + ), + TypeHintMatch( + function_name="frontend_stream_type", + return_type=["StreamType", None], + ), + TypeHintMatch( + function_name="available", + return_type="bool", + ), + TypeHintMatch( + function_name="async_create_stream", + return_type=["Stream", None], + ), + TypeHintMatch( + function_name="stream_source", + return_type=["str", None], + ), + TypeHintMatch( + function_name="async_handle_web_rtc_offer", + arg_types={ + 1: "str", + }, + return_type=["str", None], + ), + TypeHintMatch( + function_name="camera_image", + named_arg_types={ + "width": "int | None", + "height": "int | None", + }, + return_type=["bytes", None], + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="handle_async_still_stream", + arg_types={ + 1: "Request", + 2: "float", + }, + return_type="StreamResponse", + ), + TypeHintMatch( + function_name="handle_async_mjpeg_stream", + arg_types={ + 1: "Request", + }, + return_type=["StreamResponse", None], + ), + TypeHintMatch( + function_name="is_on", + return_type="bool", + ), + TypeHintMatch( + function_name="turn_off", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_on", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="enable_motion_detection", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="disable_motion_detection", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], + "climate": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ClimateEntity", + matches=[ + TypeHintMatch( + function_name="precision", + return_type="float", + ), + TypeHintMatch( + function_name="temperature_unit", + return_type="str", + ), + TypeHintMatch( + function_name="current_humidity", + return_type=["int", None], + ), + TypeHintMatch( + function_name="target_humidity", + return_type=["int", None], + ), + TypeHintMatch( + function_name="hvac_mode", + return_type=["HVACMode", "str", None], + ), + TypeHintMatch( + function_name="hvac_modes", + return_type=["list[HVACMode]", "list[str]"], + ), + TypeHintMatch( + function_name="hvac_action", + return_type=["HVACAction", "str", None], + ), + TypeHintMatch( + function_name="current_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_step", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_high", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_low", + return_type=["float", None], + ), + TypeHintMatch( + function_name="preset_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="preset_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="is_aux_heat", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="fan_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="fan_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="swing_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="swing_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="set_temperature", + kwargs_type="Any", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_humidity", + arg_types={ + 1: "int", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_fan_mode", + arg_types={ + 1: "str", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_hvac_mode", + arg_types={ + 1: "HVACMode", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_swing_mode", + arg_types={ + 1: "str", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_preset_mode", + arg_types={ + 1: "str", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_aux_heat_on", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_aux_heat_off", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_on", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_off", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="min_temp", + return_type="float", + ), + TypeHintMatch( + function_name="max_temp", + return_type="float", + ), + TypeHintMatch( + function_name="min_humidity", + return_type="int", + ), + TypeHintMatch( + function_name="max_humidity", + return_type="int", + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", @@ -893,6 +1210,33 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "geo_location": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="GeolocationEvent", + matches=[ + TypeHintMatch( + function_name="source", + return_type="str", + ), + TypeHintMatch( + function_name="distance", + return_type=["float", None], + ), + TypeHintMatch( + function_name="latitude", + return_type=["float", None], + ), + TypeHintMatch( + function_name="longitude", + return_type=["float", None], + ), + ], + ), + ], "light": [ ClassTypeHintMatch( base_class="Entity", @@ -1099,25 +1443,26 @@ def _is_valid_type( and _is_valid_type(match.group(2), node.right) ) - # Special case for xxx[yyy[zzz, aaa]]` - if match := _TYPE_HINT_MATCHERS["x_of_y_of_z_comma_a"].match(expected_type): - return ( - isinstance(node, nodes.Subscript) - and _is_valid_type(match.group(1), node.value) - and isinstance(subnode := node.slice, nodes.Subscript) - and _is_valid_type(match.group(2), subnode.value) - and isinstance(subnode.slice, nodes.Tuple) - and _is_valid_type(match.group(3), subnode.slice.elts[0]) - and _is_valid_type(match.group(4), subnode.slice.elts[1]) + # Special case for `xxx[aaa, bbb, ccc, ...] + if ( + isinstance(node, nodes.Subscript) + and isinstance(node.slice, nodes.Tuple) + and ( + match := _TYPE_HINT_MATCHERS[f"x_of_y_{len(node.slice.elts)}"].match( + expected_type + ) ) - - # Special case for xxx[yyy, zzz]` - if match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(expected_type): - # Handle special case of Mapping[xxx, Any] - if in_return and match.group(1) == "Mapping" and match.group(3) == "Any": + ): + # This special case is separate because we want Mapping[str, Any] + # to also match dict[str, int] and similar + if ( + len(node.slice.elts) == 2 + and in_return + and match.group(1) == "Mapping" + and match.group(3) == "Any" + ): return ( - isinstance(node, nodes.Subscript) - and isinstance(node.value, nodes.Name) + isinstance(node.value, nodes.Name) # We accept dict when Mapping is needed and node.value.name in ("Mapping", "dict") and isinstance(node.slice, nodes.Tuple) @@ -1125,16 +1470,19 @@ def _is_valid_type( # Ignore second item # and _is_valid_type(match.group(3), node.slice.elts[1]) ) + + # This is the default case return ( - isinstance(node, nodes.Subscript) - and _is_valid_type(match.group(1), node.value) + _is_valid_type(match.group(1), node.value) and isinstance(node.slice, nodes.Tuple) - and _is_valid_type(match.group(2), node.slice.elts[0]) - and _is_valid_type(match.group(3), node.slice.elts[1]) + and all( + _is_valid_type(match.group(n + 2), node.slice.elts[n]) + for n in range(len(node.slice.elts)) + ) ) - # Special case for xxx[yyy]` - if match := _TYPE_HINT_MATCHERS["x_of_y"].match(expected_type): + # Special case for xxx[yyy] + if match := _TYPE_HINT_MATCHERS["x_of_y_1"].match(expected_type): return ( isinstance(node, nodes.Subscript) and _is_valid_type(match.group(1), node.value) @@ -1243,7 +1591,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ( "ignore-missing-annotations", { - "default": True, + "default": False, "type": "yn", "metavar": "", "help": "Set to ``no`` if you wish to check functions that do not " diff --git a/pyproject.toml b/pyproject.toml index 97a9c51dbd6..c7e187e07f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.7" +version = "2022.8.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -36,13 +36,15 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.23.0", + "home-assistant-bluetooth==1.3.0", "ifaddr==0.1.7", "jinja2==3.1.2", + "lru-dict==1.1.8", "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", - "orjson==3.7.5", - "pip>=21.0,<22.2", + "orjson==3.7.8", + "pip>=21.0,<22.3", "python-slugify==4.0.1", "pyyaml==6.0", "requests==2.28.1", @@ -109,7 +111,6 @@ init-hook = """\ load-plugins = [ "pylint.extensions.code_style", "pylint.extensions.typing", - "pylint_strict_informational", "hass_constructor", "hass_enforce_type_hints", "hass_imports", @@ -123,6 +124,9 @@ extension-pkg-allow-list = [ "orjson", "cv2", ] +fail-on = [ + "I", +] [tool.pylint.BASIC] class-const-naming-style = "any" diff --git a/requirements.txt b/requirements.txt index c345cac25c9..ce77253b752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,12 +11,14 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 httpx==0.23.0 +home-assistant-bluetooth==1.3.0 ifaddr==0.1.7 jinja2==3.1.2 +lru-dict==1.1.8 PyJWT==2.4.0 cryptography==36.0.2 -orjson==3.7.5 -pip>=21.0,<22.2 +orjson==3.7.8 +pip>=21.0,<22.3 python-slugify==4.0.1 pyyaml==6.0 requests==2.28.1 diff --git a/requirements_all.txt b/requirements_all.txt index f33648aec8a..6e1850d33a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.27 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 @@ -37,17 +37,17 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.6 +PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.16.4 +PyViCare==2.16.2 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 @@ -77,7 +77,7 @@ accuweather==0.3.0 adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.2 +adb-shell[async]==0.4.3 # homeassistant.components.alarmdecoder adext==0.4.2 @@ -89,7 +89,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.2.6 +afsapi==0.2.7 # homeassistant.components.agent_dvr agent-py==0.0.23 @@ -113,7 +113,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.5 +aioairzone==0.4.6 # homeassistant.components.ambient_station aioambient==2021.11.0 @@ -128,7 +128,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.6.0 +aiobafi6==0.7.0 # homeassistant.components.aws aiobotocore==2.1.0 @@ -150,7 +150,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.10.0 +aioesphomeapi==10.11.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -162,13 +162,13 @@ aioftp==0.12.0 aiogithubapi==22.2.4 # homeassistant.components.guardian -aioguardian==2022.03.2 +aioguardian==2022.07.0 # homeassistant.components.harmony aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.20 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -186,6 +186,9 @@ aiokafka==0.7.2 # homeassistant.components.kef aiokef==0.2.16 +# homeassistant.components.lifx +aiolifx-connection==1.0.0 + # homeassistant.components.lifx aiolifx==0.8.1 @@ -226,7 +229,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.6.0 +aiopyarr==22.7.0 # homeassistant.components.qnap_qsw aioqsw==0.1.1 @@ -310,7 +313,7 @@ androidtv[async]==0.0.67 anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anthemav -anthemav==1.2.0 +anthemav==1.3.2 # homeassistant.components.apcupsd apcaccess==0.0.13 @@ -372,7 +375,7 @@ axis==44 azure-eventhub==5.7.0 # homeassistant.components.azure_service_bus -azure-servicebus==0.50.3 +azure-servicebus==7.8.0 # homeassistant.components.baidu baidu-aip==1.6.6 @@ -393,7 +396,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.1 +bellows==0.31.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -401,8 +404,11 @@ bimmer_connected==0.10.1 # homeassistant.components.bizkaibus bizkaibus==0.1.1 +# homeassistant.components.bluetooth +bleak==0.15.0 + # homeassistant.components.blebox -blebox_uniapi==2.0.1 +blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 @@ -414,10 +420,12 @@ blinkstick==1.2.0 blockchain==1.4.4 # homeassistant.components.decora -# homeassistant.components.miflora # homeassistant.components.zengge # bluepy==1.3.0 +# homeassistant.components.bluetooth +bluetooth-adapters==0.1.3 + # homeassistant.components.bond bond-async==0.1.22 @@ -518,7 +526,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.0 +debugpy==1.6.2 # homeassistant.components.decora # decora==0.6 @@ -598,7 +606,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.entur_public_transport -enturclient==0.2.3 +enturclient==0.2.4 # homeassistant.components.environment_canada env_canada==0.5.22 @@ -635,7 +643,7 @@ faadelays==0.0.7 fastdotcom==0.0.3 # homeassistant.components.feedreader -feedparser==6.0.2 +feedparser==6.0.10 # homeassistant.components.fibaro fiblary3==0.1.8 @@ -734,13 +742,13 @@ glances_api==0.3.5 goalzero==0.2.1 # homeassistant.components.goodwe -goodwe==0.2.15 +goodwe==0.2.18 # homeassistant.components.google_pubsub google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==2.11.1 +google-cloud-texttospeech==2.12.0 # homeassistant.components.nest google-nest-sdm==2.0.0 @@ -751,6 +759,9 @@ googlemaps==2.5.1 # homeassistant.components.slide goslide-api==0.5.1 +# homeassistant.components.govee_ble +govee-ble==0.12.6 + # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 @@ -828,13 +839,13 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.1 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.4 +homematicip==1.0.7 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 @@ -890,6 +901,9 @@ influxdb-client==1.24.0 # homeassistant.components.influxdb influxdb==5.3.1 +# homeassistant.components.inkbird +inkbird-ble==0.5.1 + # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 @@ -974,9 +988,6 @@ logi_circle==0.2.3 # homeassistant.components.london_underground london-tube-status==0.5 -# homeassistant.components.recorder -lru-dict==1.1.7 - # homeassistant.components.luftdaten luftdaten==0.7.2 @@ -987,7 +998,7 @@ lupupy==0.0.24 lw12==0.9.2 # homeassistant.components.scrape -lxml==4.8.0 +lxml==4.9.1 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 @@ -1014,7 +1025,7 @@ meater-python==0.0.8 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.2.0 +meteoalertapi==0.3.0 # homeassistant.components.meteo_france meteofrance-api==1.0.2 @@ -1025,9 +1036,6 @@ mficlient==0.3.0 # homeassistant.components.xiaomi_miio micloud==0.5 -# homeassistant.components.miflora -miflora==0.7.2 - # homeassistant.components.mill mill-local==0.1.1 @@ -1037,14 +1045,14 @@ millheater==0.9.0 # homeassistant.components.minio minio==5.0.10 -# homeassistant.components.mitemp_bt -mitemp_bt==0.0.5 +# homeassistant.components.moat +moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.8 +motionblinds==0.6.11 # homeassistant.components.motioneye motioneye-client==0.3.12 @@ -1086,7 +1094,7 @@ nettigo-air-monitor==1.3.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.1 +nexia==2.0.2 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 @@ -1094,6 +1102,9 @@ nextcloudmonitor==1.1.0 # homeassistant.components.discord nextcord==2.0.0a8 +# homeassistant.components.nextdns +nextdns==1.0.1 + # homeassistant.components.niko_home_control niko-home-control==0.2.1 @@ -1126,7 +1137,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.0 +numpy==1.23.1 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -1236,7 +1247,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.1.1 +pillow==9.2.0 # homeassistant.components.dominos pizzapi==0.0.3 @@ -1251,7 +1262,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.5 +plugwise==0.18.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1281,7 +1292,7 @@ prometheus_client==0.7.1 proxmoxer==1.3.1 # homeassistant.components.systemmonitor -psutil==5.9.0 +psutil==5.9.1 # homeassistant.components.pulseaudio_loopback pulsectl==20.2.4 @@ -1339,7 +1350,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.switchmate -# pySwitchmate==0.4.6 +pySwitchmate==0.5.1 # homeassistant.components.tibber pyTibber==0.22.3 @@ -1381,7 +1392,7 @@ pyatmo==6.2.4 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 @@ -1444,7 +1455,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==98 +pydeconz==101 # homeassistant.components.delijn pydelijn==1.0.0 @@ -1524,7 +1535,6 @@ pyfronius==0.7.1 # homeassistant.components.ifttt pyfttt==0.3 -# homeassistant.components.bluetooth_le_tracker # homeassistant.components.skybeacon pygatt[GATTTOOL]==4.0.5 @@ -1559,7 +1569,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.intesishome pyintesishome==1.8.0 @@ -1640,7 +1650,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.6 +pymazda==0.3.7 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 @@ -1715,7 +1725,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -1774,7 +1784,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.3.1 +pyrisco==0.5.0 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -1792,7 +1802,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.17 +pysensibo==1.0.18 # homeassistant.components.serial # homeassistant.components.zha @@ -1883,7 +1893,7 @@ python-digitalocean==1.13.2 python-ecobee-api==0.2.14 # homeassistant.components.eq3btsmart -# python-eq3bt==0.1.11 +# python-eq3bt==0.2 # homeassistant.components.etherscan python-etherscan-api==0.0.3 @@ -1904,7 +1914,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.0.3 +python-homewizard-energy==1.1.0 # homeassistant.components.hp_ilo python-hpilo==4.3 @@ -1925,7 +1935,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.11 +python-miio==0.5.12 # homeassistant.components.mpd python-mpd2==3.0.5 @@ -1979,7 +1989,7 @@ pytomorrowio==0.3.4 pytouchline==0.7 # homeassistant.components.traccar -pytraccar==0.10.0 +pytraccar==1.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.0 @@ -1990,10 +2000,10 @@ pytradfri[async]==9.0.0 pytrafikverket==0.2.0.1 # homeassistant.components.usb -pyudev==0.22.0 +pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.10 +pyunifiprotect==4.0.11 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2047,7 +2057,7 @@ pyzerproc==0.4.8 qnapstats==0.4.0 # homeassistant.components.quantum_gateway -quantum-gateway==0.0.6 +quantum-gateway==0.0.8 # homeassistant.components.rachio rachiopy==1.0.3 @@ -2113,7 +2123,7 @@ rtsp-to-webrtc==0.5.1 russound==0.1.9 # homeassistant.components.russound_rio -russound_rio==0.1.7 +russound_rio==0.1.8 # homeassistant.components.yamaha rxv==0.7.0 @@ -2149,8 +2159,11 @@ sendgrid==6.8.2 # homeassistant.components.sense sense_energy==0.10.4 +# homeassistant.components.sensorpush +sensorpush-ble==1.5.1 + # homeassistant.components.sentry -sentry-sdk==1.6.0 +sentry-sdk==1.8.0 # homeassistant.components.sharkiq sharkiq==0.0.1 @@ -2159,7 +2172,7 @@ sharkiq==0.0.1 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.27.0 +shodan==1.28.0 # homeassistant.components.sighthound simplehound==0.3 @@ -2355,7 +2368,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.4 +unifi-discovery==1.1.5 # homeassistant.components.unifiled unifiled==0.11 @@ -2369,7 +2382,7 @@ upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru # homeassistant.components.zwave_me -url-normalize==1.4.1 +url-normalize==1.4.3 # homeassistant.components.uscis uscisstatus==0.1.1 @@ -2378,7 +2391,7 @@ uscisstatus==0.1.1 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.11.0 +vallox-websocket-api==2.12.0 # homeassistant.components.rdw vehicle==0.4.0 @@ -2399,7 +2412,7 @@ volkszaehler==0.3.2 volvooncall==0.10.0 # homeassistant.components.verisure -vsure==1.7.3 +vsure==1.8.1 # homeassistant.components.vasttrafik vtjp==0.1.14 @@ -2412,7 +2425,7 @@ vultr==0.1.2 # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan -wakeonlan==2.0.1 +wakeonlan==2.1.0 # homeassistant.components.wallbox wallbox==0.4.9 @@ -2445,7 +2458,7 @@ wirelesstagpy==0.8.1 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.2 +wled==0.14.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 @@ -2456,8 +2469,11 @@ xbox-webapi==2.0.11 # homeassistant.components.xbox_live xboxapi==2.0.1 +# homeassistant.components.xiaomi_ble +xiaomi-ble==0.6.4 + # homeassistant.components.knx -xknx==0.21.5 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -2483,7 +2499,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.8 +yolink-api==0.0.9 # homeassistant.components.youless youless-api==0.16 @@ -2498,7 +2514,7 @@ zengge==0.2 zeroconf==0.38.7 # homeassistant.components.zha -zha-quirks==0.0.77 +zha-quirks==0.0.78 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2519,7 +2535,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.3 +zigpy==0.48.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test.txt b/requirements_test.txt index 6072ce896ee..2331e971711 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,28 +8,27 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.4.1 +coverage==6.4.2 freezegun==1.2.1 mock-open==1.4.0 -mypy==0.961 -pre-commit==2.19.0 -pylint==2.14.3 +mypy==0.971 +pre-commit==2.20.0 +pylint==2.14.4 pipdeptree==2.2.1 -pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==3.0.0 pytest-freezegun==0.4.2 pytest-socket==0.5.1 pytest-test-groups==1.0.3 -pytest-sugar==0.9.4 +pytest-sugar==0.9.5 pytest-timeout==2.1.0 pytest-xdist==2.5.0 pytest==7.1.2 requests_mock==1.9.2 -respx==0.19.0 +respx==0.19.2 stdlib-list==0.7.0 tomli==2.0.1;python_version<"3.11" -tqdm==4.49.0 +tqdm==4.64.0 types-atomicwrites==1.4.1 types-croniter==1.0.0 types-backports==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e11f13233e..6f75b540b28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.27 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 @@ -33,17 +33,17 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.6 +PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.16.4 +PyViCare==2.16.2 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 @@ -67,7 +67,7 @@ accuweather==0.3.0 adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.2 +adb-shell[async]==0.4.3 # homeassistant.components.alarmdecoder adext==0.4.2 @@ -100,7 +100,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.5 +aioairzone==0.4.6 # homeassistant.components.ambient_station aioambient==2021.11.0 @@ -115,7 +115,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.6.0 +aiobafi6==0.7.0 # homeassistant.components.aws aiobotocore==2.1.0 @@ -137,7 +137,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.10.0 +aioesphomeapi==10.11.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -146,13 +146,13 @@ aioflo==2021.11.0 aiogithubapi==22.2.4 # homeassistant.components.guardian -aioguardian==2022.03.2 +aioguardian==2022.07.0 # homeassistant.components.harmony aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.20 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -164,6 +164,15 @@ aiohue==4.4.2 # homeassistant.components.apache_kafka aiokafka==0.7.2 +# homeassistant.components.lifx +aiolifx-connection==1.0.0 + +# homeassistant.components.lifx +aiolifx==0.8.1 + +# homeassistant.components.lifx +aiolifx_effects==0.2.2 + # homeassistant.components.lookin aiolookin==0.1.1 @@ -195,7 +204,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.6.0 +aiopyarr==22.7.0 # homeassistant.components.qnap_qsw aioqsw==0.1.1 @@ -269,6 +278,9 @@ ambiclimate==0.2.1 # homeassistant.components.androidtv androidtv[async]==0.0.67 +# homeassistant.components.anthemav +anthemav==1.3.2 + # homeassistant.components.apprise apprise==0.9.9 @@ -308,17 +320,23 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.1 +bellows==0.31.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 +# homeassistant.components.bluetooth +bleak==0.15.0 + # homeassistant.components.blebox -blebox_uniapi==2.0.1 +blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 +# homeassistant.components.bluetooth +bluetooth-adapters==0.1.3 + # homeassistant.components.bond bond-async==0.1.22 @@ -385,7 +403,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.0 +debugpy==1.6.2 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -453,7 +471,7 @@ epson-projector==0.4.6 faadelays==0.0.7 # homeassistant.components.feedreader -feedparser==6.0.2 +feedparser==6.0.10 # homeassistant.components.fibaro fiblary3==0.1.8 @@ -531,7 +549,7 @@ glances_api==0.3.5 goalzero==0.2.1 # homeassistant.components.goodwe -goodwe==0.2.15 +goodwe==0.2.18 # homeassistant.components.google_pubsub google-cloud-pubsub==2.11.0 @@ -542,6 +560,9 @@ google-nest-sdm==2.0.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 +# homeassistant.components.govee_ble +govee-ble==0.12.6 + # homeassistant.components.gree greeclimate==1.2.0 @@ -595,13 +616,13 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.1 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.4 +homematicip==1.0.7 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 @@ -633,6 +654,9 @@ influxdb-client==1.24.0 # homeassistant.components.influxdb influxdb==5.3.1 +# homeassistant.components.inkbird +inkbird-ble==0.5.1 + # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 @@ -678,14 +702,11 @@ life360==4.1.1 # homeassistant.components.logi_circle logi_circle==0.2.3 -# homeassistant.components.recorder -lru-dict==1.1.7 - # homeassistant.components.luftdaten luftdaten==0.7.2 # homeassistant.components.scrape -lxml==4.8.0 +lxml==4.9.1 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 @@ -720,11 +741,14 @@ millheater==0.9.0 # homeassistant.components.minio minio==5.0.10 +# homeassistant.components.moat +moat-ble==0.1.1 + # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.8 +motionblinds==0.6.11 # homeassistant.components.motioneye motioneye-client==0.3.12 @@ -754,11 +778,14 @@ netmap==0.7.0.2 nettigo-air-monitor==1.3.0 # homeassistant.components.nexia -nexia==2.0.1 +nexia==2.0.2 # homeassistant.components.discord nextcord==2.0.0a8 +# homeassistant.components.nextdns +nextdns==1.0.1 + # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 @@ -779,7 +806,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.0 +numpy==1.23.1 # homeassistant.components.google oauth2client==4.1.3 @@ -844,7 +871,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.1.1 +pillow==9.2.0 # homeassistant.components.plex plexapi==4.11.2 @@ -856,7 +883,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.5 +plugwise==0.18.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -941,7 +968,7 @@ pyatag==0.3.5.3 pyatmo==6.2.4 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 @@ -974,7 +1001,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==98 +pydeconz==101 # homeassistant.components.dexcom pydexcom==0.2.3 @@ -1027,10 +1054,6 @@ pyfronius==0.7.1 # homeassistant.components.ifttt pyfttt==0.3 -# homeassistant.components.bluetooth_le_tracker -# homeassistant.components.skybeacon -pygatt[GATTTOOL]==4.0.5 - # homeassistant.components.hvv_departures pygti==0.9.3 @@ -1053,7 +1076,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.ipma pyipma==2.0.5 @@ -1113,7 +1136,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.6 +pymazda==0.3.7 # homeassistant.components.melcloud pymelcloud==2.5.6 @@ -1167,7 +1190,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -1205,7 +1228,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.3.1 +pyrisco==0.5.0 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -1217,7 +1240,7 @@ pyruckus==0.16 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.17 +pysensibo==1.0.18 # homeassistant.components.serial # homeassistant.components.zha @@ -1275,7 +1298,7 @@ python-ecobee-api==0.2.14 python-forecastio==1.4.0 # homeassistant.components.homewizard -python-homewizard-energy==1.0.3 +python-homewizard-energy==1.1.0 # homeassistant.components.izone python-izone==1.2.3 @@ -1287,7 +1310,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.xiaomi_miio -python-miio==0.5.11 +python-miio==0.5.12 # homeassistant.components.nest python-nest==4.2.0 @@ -1317,7 +1340,7 @@ pytile==2022.02.0 pytomorrowio==0.3.4 # homeassistant.components.traccar -pytraccar==0.10.0 +pytraccar==1.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.0 @@ -1328,10 +1351,10 @@ pytradfri[async]==9.0.0 pytrafikverket==0.2.0.1 # homeassistant.components.usb -pyudev==0.22.0 +pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.10 +pyunifiprotect==4.0.11 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -1427,8 +1450,11 @@ securetar==2022.2.0 # homeassistant.components.sense sense_energy==0.10.4 +# homeassistant.components.sensorpush +sensorpush-ble==1.5.1 + # homeassistant.components.sentry -sentry-sdk==1.6.0 +sentry-sdk==1.8.0 # homeassistant.components.sharkiq sharkiq==0.0.1 @@ -1564,7 +1590,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.4 +unifi-discovery==1.1.5 # homeassistant.components.upb upb_lib==0.4.12 @@ -1575,13 +1601,13 @@ upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru # homeassistant.components.zwave_me -url-normalize==1.4.1 +url-normalize==1.4.3 # homeassistant.components.uvc uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.11.0 +vallox-websocket-api==2.12.0 # homeassistant.components.rdw vehicle==0.4.0 @@ -1596,7 +1622,7 @@ venstarcolortouch==0.18 vilfo-api-client==0.3.2 # homeassistant.components.verisure -vsure==1.7.3 +vsure==1.8.1 # homeassistant.components.vulcan vulcan-api==2.1.1 @@ -1606,7 +1632,7 @@ vultr==0.1.2 # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan -wakeonlan==2.0.1 +wakeonlan==2.1.0 # homeassistant.components.wallbox wallbox==0.4.9 @@ -1627,7 +1653,7 @@ wiffi==1.1.0 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.2 +wled==0.14.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 @@ -1635,8 +1661,11 @@ wolf_smartset==0.1.11 # homeassistant.components.xbox xbox-webapi==2.0.11 +# homeassistant.components.xiaomi_ble +xiaomi-ble==0.6.4 + # homeassistant.components.knx -xknx==0.21.5 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -1656,7 +1685,7 @@ yalexs==1.1.25 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.8 +yolink-api==0.0.9 # homeassistant.components.youless youless-api==0.16 @@ -1665,7 +1694,7 @@ youless-api==0.16 zeroconf==0.38.7 # homeassistant.components.zha -zha-quirks==0.0.77 +zha-quirks==0.0.78 # homeassistant.components.zha zigpy-deconz==0.18.0 @@ -1680,7 +1709,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.3 +zigpy==0.48.0 # homeassistant.components.zwave_js zwave-js-server-python==0.39.0 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 0d204771e40..c7f5d559c38 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,16 +1,16 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.4 -black==22.3.0 +black==22.6.0 codespell==2.1.0 -flake8-comprehensions==3.8.0 +flake8-comprehensions==3.10.0 flake8-docstrings==1.6.0 -flake8-noqa==1.2.1 +flake8-noqa==1.2.5 flake8==4.0.1 isort==5.10.1 mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.34.0 -yamllint==1.26.3 +pyupgrade==2.37.2 +yamllint==1.27.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7f77f4ae106..5ef794b4eab 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -30,7 +30,6 @@ COMMENT_REQUIREMENTS = ( "opencv-python-headless", "pybluez", "pycups", - "pySwitchmate", "python-eq3bt", "python-gammu", "python-lirc", @@ -106,7 +105,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.0 +numpy==1.23.1 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 889cad2a497..233abda4ed8 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -6,6 +6,7 @@ from time import monotonic from . import ( application_credentials, + bluetooth, codeowners, config_flow, coverage, @@ -19,6 +20,7 @@ from . import ( requirements, services, ssdp, + supported_brands, translations, usb, zeroconf, @@ -27,6 +29,7 @@ from .model import Config, Integration INTEGRATION_PLUGINS = [ application_credentials, + bluetooth, codeowners, config_flow, dependencies, @@ -37,6 +40,7 @@ INTEGRATION_PLUGINS = [ requirements, services, ssdp, + supported_brands, translations, usb, zeroconf, diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py new file mode 100644 index 00000000000..d8277213f27 --- /dev/null +++ b/script/hassfest/bluetooth.py @@ -0,0 +1,65 @@ +"""Generate bluetooth file.""" +from __future__ import annotations + +import json + +from .model import Config, Integration + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +\"\"\" +from __future__ import annotations + +# fmt: off + +BLUETOOTH: list[dict[str, str | int | list[int]]] = {} +""".strip() + + +def generate_and_validate(integrations: list[dict[str, str]]): + """Validate and generate bluetooth data.""" + match_list = [] + + for domain in sorted(integrations): + integration = integrations[domain] + + if not integration.manifest or not integration.config_flow: + continue + + match_types = integration.manifest.get("bluetooth", []) + + if not match_types: + continue + + for entry in match_types: + match_list.append({"domain": domain, **entry}) + + return BASE.format(json.dumps(match_list, indent=4)) + + +def validate(integrations: dict[str, Integration], config: Config): + """Validate bluetooth file.""" + bluetooth_path = config.root / "homeassistant/generated/bluetooth.py" + config.cache["bluetooth"] = content = generate_and_validate(integrations) + + if config.specific_integrations: + return + + with open(str(bluetooth_path)) as fp: + current = fp.read().strip() + if current != content: + config.add_error( + "bluetooth", + "File bluetooth.py is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + return + + +def generate(integrations: dict[str, Integration], config: Config): + """Generate bluetooth file.""" + bluetooth_path = config.root / "homeassistant/generated/bluetooth.py" + with open(str(bluetooth_path), "w") as fp: + fp.write(f"{config.cache['bluetooth']}\n") diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 169ccedf4a1..ad4a1d79229 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -35,6 +35,7 @@ def validate_integration(config: Config, integration: Integration): needs_unique_id = integration.domain not in UNIQUE_ID_IGNORE and ( "async_step_discovery" in config_flow + or "async_step_bluetooth" in config_flow or "async_step_hassio" in config_flow or "async_step_homekit" in config_flow or "async_step_mqtt" in config_flow diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 0cd20364533..5ce67b59198 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -56,6 +56,7 @@ NO_IOT_CLASS = [ "hardware", "history", "homeassistant", + "homeassistant_alerts", "homeassistant_yellow", "image", "input_boolean", @@ -80,6 +81,7 @@ NO_IOT_CLASS = [ "proxy", "python_script", "raspberry_pi", + "repairs", "safe_mode", "script", "search", @@ -189,6 +191,17 @@ MANIFEST_SCHEMA = vol.Schema( vol.Optional("ssdp"): vol.Schema( vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), + vol.Optional("bluetooth"): [ + vol.Schema( + { + vol.Optional("service_uuid"): vol.All(str, verify_lowercase), + vol.Optional("service_data_uuid"): vol.All(str, verify_lowercase), + vol.Optional("local_name"): vol.All(str), + vol.Optional("manufacturer_id"): int, + vol.Optional("manufacturer_data_start"): [int], + } + ) + ], vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}), vol.Optional("dhcp"): [ vol.Schema( diff --git a/script/hassfest/model.py b/script/hassfest/model.py index fc38e1db592..d4e1fbf806a 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -112,6 +112,11 @@ class Integration: """List of dependencies.""" return self.manifest.get("dependencies", []) + @property + def supported_brands(self) -> dict[str]: + """Return dict of supported brands.""" + return self.manifest.get("supported_brands", {}) + @property def integration_type(self) -> str: """Get integration_type.""" diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b4df9e00495..4bf8cfd68cf 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,87 +16,6 @@ from .model import Config, Integration # remove your component from this list to enable type checks. # Do your best to not add anything new here. IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.blueprint.importer", - "homeassistant.components.blueprint.models", - "homeassistant.components.blueprint.websocket_api", - "homeassistant.components.cloud.client", - "homeassistant.components.cloud.http_api", - "homeassistant.components.conversation", - "homeassistant.components.conversation.default_agent", - "homeassistant.components.denonavr.config_flow", - "homeassistant.components.denonavr.media_player", - "homeassistant.components.denonavr.receiver", - "homeassistant.components.evohome", - "homeassistant.components.evohome.climate", - "homeassistant.components.evohome.water_heater", - "homeassistant.components.google_assistant.helpers", - "homeassistant.components.google_assistant.http", - "homeassistant.components.google_assistant.report_state", - "homeassistant.components.google_assistant.trait", - "homeassistant.components.gree.climate", - "homeassistant.components.gree.switch", - "homeassistant.components.harmony", - "homeassistant.components.harmony.config_flow", - "homeassistant.components.harmony.data", - "homeassistant.components.hassio", - "homeassistant.components.hassio.auth", - "homeassistant.components.hassio.binary_sensor", - "homeassistant.components.hassio.ingress", - "homeassistant.components.hassio.sensor", - "homeassistant.components.hassio.system_health", - "homeassistant.components.hassio.websocket_api", - "homeassistant.components.home_plus_control", - "homeassistant.components.home_plus_control.api", - "homeassistant.components.icloud", - "homeassistant.components.icloud.account", - "homeassistant.components.icloud.device_tracker", - "homeassistant.components.icloud.sensor", - "homeassistant.components.influxdb", - "homeassistant.components.input_datetime", - "homeassistant.components.izone.climate", - "homeassistant.components.konnected", - "homeassistant.components.konnected.config_flow", - "homeassistant.components.kostal_plenticore.helper", - "homeassistant.components.kostal_plenticore.select", - "homeassistant.components.kostal_plenticore.sensor", - "homeassistant.components.kostal_plenticore.switch", - "homeassistant.components.lovelace", - "homeassistant.components.lovelace.dashboard", - "homeassistant.components.lovelace.resources", - "homeassistant.components.lovelace.websocket", - "homeassistant.components.lyric.climate", - "homeassistant.components.lyric.config_flow", - "homeassistant.components.lyric.sensor", - "homeassistant.components.melcloud", - "homeassistant.components.melcloud.climate", - "homeassistant.components.meteo_france.sensor", - "homeassistant.components.meteo_france.weather", - "homeassistant.components.minecraft_server", - "homeassistant.components.minecraft_server.helpers", - "homeassistant.components.minecraft_server.sensor", - "homeassistant.components.nilu.air_quality", - "homeassistant.components.nzbget", - "homeassistant.components.nzbget.config_flow", - "homeassistant.components.nzbget.coordinator", - "homeassistant.components.nzbget.switch", - "homeassistant.components.omnilogic.common", - "homeassistant.components.omnilogic.sensor", - "homeassistant.components.omnilogic.switch", - "homeassistant.components.onvif.base", - "homeassistant.components.onvif.binary_sensor", - "homeassistant.components.onvif.camera", - "homeassistant.components.onvif.device", - "homeassistant.components.onvif.sensor", - "homeassistant.components.philips_js", - "homeassistant.components.philips_js.config_flow", - "homeassistant.components.philips_js.device_trigger", - "homeassistant.components.philips_js.light", - "homeassistant.components.philips_js.media_player", - "homeassistant.components.plex.media_player", - "homeassistant.components.profiler", - "homeassistant.components.solaredge.config_flow", - "homeassistant.components.solaredge.coordinator", - "homeassistant.components.solaredge.sensor", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", @@ -109,38 +28,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.sensor", "homeassistant.components.sonos.speaker", "homeassistant.components.sonos.statistics", - "homeassistant.components.system_health", - "homeassistant.components.telegram_bot.polling", - "homeassistant.components.template.number", - "homeassistant.components.template.sensor", - "homeassistant.components.toon", - "homeassistant.components.toon.config_flow", - "homeassistant.components.toon.models", - "homeassistant.components.unifi", - "homeassistant.components.unifi.config_flow", - "homeassistant.components.unifi.device_tracker", - "homeassistant.components.unifi.diagnostics", - "homeassistant.components.unifi.unifi_entity_base", - "homeassistant.components.withings", - "homeassistant.components.withings.binary_sensor", - "homeassistant.components.withings.common", - "homeassistant.components.withings.config_flow", - "homeassistant.components.xbox", - "homeassistant.components.xbox.base_sensor", - "homeassistant.components.xbox.binary_sensor", - "homeassistant.components.xbox.browse_media", - "homeassistant.components.xbox.media_source", - "homeassistant.components.xbox.sensor", - "homeassistant.components.xiaomi_miio", - "homeassistant.components.xiaomi_miio.air_quality", - "homeassistant.components.xiaomi_miio.binary_sensor", - "homeassistant.components.xiaomi_miio.device", - "homeassistant.components.xiaomi_miio.device_tracker", - "homeassistant.components.xiaomi_miio.fan", - "homeassistant.components.xiaomi_miio.humidifier", - "homeassistant.components.xiaomi_miio.light", - "homeassistant.components.xiaomi_miio.sensor", - "homeassistant.components.xiaomi_miio.switch", ] # Component modules which should set no_implicit_reexport = true. diff --git a/script/hassfest/supported_brands.py b/script/hassfest/supported_brands.py new file mode 100644 index 00000000000..6740260a04c --- /dev/null +++ b/script/hassfest/supported_brands.py @@ -0,0 +1,55 @@ +"""Generate supported_brands data.""" +from __future__ import annotations + +import json + +from .model import Config, Integration + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +\"\"\" + +# fmt: off + +HAS_SUPPORTED_BRANDS = ({}) +""".strip() + + +def generate_and_validate(integrations: dict[str, Integration], config: Config) -> str: + """Validate and generate supported_brands data.""" + + brands = [ + domain + for domain, integration in sorted(integrations.items()) + if integration.supported_brands + ] + + return BASE.format(json.dumps(brands, indent=4)[1:-1]) + + +def validate(integrations: dict[str, Integration], config: Config) -> None: + """Validate supported_brands data.""" + supported_brands_path = config.root / "homeassistant/generated/supported_brands.py" + config.cache["supported_brands"] = content = generate_and_validate( + integrations, config + ) + + if config.specific_integrations: + return + + if supported_brands_path.read_text(encoding="utf-8").strip() != content: + config.add_error( + "supported_brands", + "File supported_brands.py is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + + +def generate(integrations: dict[str, Integration], config: Config): + """Generate supported_brands data.""" + supported_brands_path = config.root / "homeassistant/generated/supported_brands.py" + supported_brands_path.write_text( + f"{config.cache['supported_brands']}\n", encoding="utf-8" + ) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index a1f520808f6..9c4f75f1b2d 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -21,7 +21,7 @@ REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" -# Only allow translatino of integration names if they contain non-brand names +# Only allow translation of integration names if they contain non-brand names ALLOW_NAME_TRANSLATION = { "cert_expiry", "cpuspeed", @@ -185,7 +185,7 @@ def gen_data_entry_schema( return vol.All(*validators) -def gen_strings_schema(config: Config, integration: Integration): +def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: """Generate a strings schema.""" return vol.Schema( { @@ -227,6 +227,25 @@ def gen_strings_schema(config: Config, integration: Integration): vol.Optional("application_credentials"): { vol.Optional("description"): cv.string_with_no_html, }, + vol.Optional("issues"): { + str: vol.All( + cv.has_at_least_one_key("description", "fix_flow"), + vol.Schema( + { + vol.Required("title"): cv.string_with_no_html, + vol.Exclusive( + "description", "fixable" + ): cv.string_with_no_html, + vol.Exclusive("fix_flow", "fixable"): gen_data_entry_schema( + config=config, + integration=integration, + flow_title=UNDEFINED, + require_step_title=False, + ), + }, + ), + ) + }, } ) diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index d1ddc177690..f4413ea4e84 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -5,7 +5,7 @@ from homeassistant import config_entries from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from homeassistant.components.NEW_DOMAIN.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -13,7 +13,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -33,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "1.1.1.1", @@ -62,7 +62,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -85,5 +85,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py index b7bef2c63f4..7d2be8a6d82 100644 --- a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.NEW_DOMAIN.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -32,7 +32,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My NEW_DOMAIN" assert result["data"] == {} assert result["options"] == { @@ -82,7 +82,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "entity_id") == input_sensor_1_entity_id @@ -93,7 +93,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "entity_id": input_sensor_2_entity_id, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_id": input_sensor_2_entity_id, "name": "My NEW_DOMAIN", diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index 035433986d4..a182de6d01f 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -100,37 +100,37 @@ async def test_login(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("pin") == str result = await hass.auth.login_flow.async_configure( result["flow_id"], {"pin": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_code" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"pin": "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"].id == "mock-id" @@ -147,9 +147,9 @@ async def test_setup_flow(hass): flow = await auth_module.async_setup_flow("new-user") result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init({"pin": "abcdefg"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert auth_module._data[1]["user_id"] == "new-user" assert auth_module._data[1]["pin"] == "abcdefg" diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 1d08ad70cc8..e69f11155a5 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -133,25 +133,25 @@ async def test_login_flow_validates_mfa(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str @@ -170,7 +170,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" @@ -187,7 +187,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" @@ -195,7 +195,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "too_many_retry" # wait service call finished @@ -203,13 +203,13 @@ async def test_login_flow_validates_mfa(hass): # restart login result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str @@ -228,7 +228,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": MOCK_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"].id == "mock-id" @@ -243,14 +243,14 @@ async def test_setup_user_notify_service(hass): flow = await notify_auth_module.async_setup_flow("test-user") step = await flow.async_step_init() - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "init" schema = step["data_schema"] schema({"notify_service": "test2"}) with patch("pyotp.HOTP.at", return_value=MOCK_CODE): step = await flow.async_step_init({"notify_service": "test1"}) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "setup" # wait service call finished @@ -266,7 +266,7 @@ async def test_setup_user_notify_service(hass): with patch("pyotp.HOTP.at", return_value=MOCK_CODE_2): step = await flow.async_step_setup({"code": "invalid"}) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "setup" assert step["errors"]["base"] == "invalid_code" @@ -283,7 +283,7 @@ async def test_setup_user_notify_service(hass): with patch("pyotp.HOTP.verify", return_value=True): step = await flow.async_step_setup({"code": MOCK_CODE_2}) - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_include_exclude_config(hass): @@ -332,7 +332,7 @@ async def test_setup_user_no_notify_service(hass): flow = await notify_auth_module.async_setup_flow("test-user") step = await flow.async_step_init() - assert step["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step["type"] == data_entry_flow.FlowResultType.ABORT assert step["reason"] == "no_available_service" @@ -369,13 +369,13 @@ async def test_not_raise_exception_when_service_not_exist(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_error" # wait service call finished diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index 2e4aad98066..53374e2d015 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -93,24 +93,24 @@ async def test_login_flow_validates_mfa(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str @@ -118,7 +118,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" @@ -126,7 +126,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": MOCK_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"].id == "mock-id" diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index e437ca9e331..22169812156 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -114,18 +114,18 @@ async def test_login_flow_validates(provider): """Test login flow.""" flow = await provider.async_login_flow({}) result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init( {"username": "bad-user", "password": "bad-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "good-user", "password": "good-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "good-user" @@ -135,5 +135,5 @@ async def test_strip_username(provider): result = await flow.async_step_init( {"username": "\t\ngood-user ", "password": "good-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "good-user" diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 62093df7210..e255135a0bd 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -115,24 +115,24 @@ async def test_login_flow_validates(data, hass): ) flow = await provider.async_login_flow({}) result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init( {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "TEST-user ", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "test-USER", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "test-USER" @@ -216,24 +216,24 @@ async def test_legacy_login_flow_validates(legacy_data, hass): ) flow = await provider.async_login_flow({}) result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init( {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "test-user" diff --git a/tests/auth/providers/test_legacy_api_password.py b/tests/auth/providers/test_legacy_api_password.py index c77cb676a6b..19af0a1e746 100644 --- a/tests/auth/providers/test_legacy_api_password.py +++ b/tests/auth/providers/test_legacy_api_password.py @@ -55,15 +55,15 @@ async def test_verify_login(hass, provider): async def test_login_flow_works(hass, manager): """Test wrong config.""" result = await manager.login_flow.async_init(handler=("legacy_api_password", None)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await manager.login_flow.async_configure( flow_id=result["flow_id"], user_input={"password": "not-hello"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await manager.login_flow.async_configure( flow_id=result["flow_id"], user_input={"password": "test-password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 406e9a033da..15e831f551a 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -10,7 +10,7 @@ from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.components.http import CONF_TRUSTED_PROXIES, CONF_USE_X_FORWARDED_FOR -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -209,7 +209,7 @@ async def test_login_flow(manager, provider): # not from trusted network flow = await provider.async_login_flow({"ip_address": ip_address("127.0.0.1")}) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, list users @@ -224,7 +224,7 @@ async def test_login_flow(manager, provider): # login with valid user step = await flow.async_step_init({"user": user.id}) - assert step["type"] == RESULT_TYPE_CREATE_ENTRY + assert step["type"] == FlowResultType.CREATE_ENTRY assert step["data"]["user"] == user.id @@ -248,7 +248,7 @@ async def test_trusted_users_login(manager_with_user, provider_with_user): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, list users intersect trusted_users @@ -332,7 +332,7 @@ async def test_trusted_group_login(manager_with_user, provider_with_user): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, list users intersect trusted_users @@ -370,7 +370,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, only one available user, bypass the login flow @@ -378,7 +378,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): {"ip_address": ip_address("192.168.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_CREATE_ENTRY + assert step["type"] == FlowResultType.CREATE_ENTRY assert step["data"]["user"] == owner.id user = await manager_bypass_login.async_create_user("test-user") diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 22d720da587..49c2c776684 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -168,12 +168,12 @@ async def test_create_new_user(hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY credential = step["result"] assert credential is not None @@ -237,12 +237,12 @@ async def test_login_as_existing_user(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY credential = step["result"] user = await manager.async_get_user_by_credentials(credential) @@ -728,14 +728,14 @@ async def test_login_with_auth_module(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) # After auth_provider validated, request auth module input form - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" step = await manager.login_flow.async_configure( @@ -743,7 +743,7 @@ async def test_login_with_auth_module(mock_hass): ) # Invalid code error - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" assert step["errors"] == {"base": "invalid_code"} @@ -752,7 +752,7 @@ async def test_login_with_auth_module(mock_hass): ) # Finally passed, get credential - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert step["result"] assert step["result"].id == "mock-id" @@ -803,21 +803,21 @@ async def test_login_with_multi_auth_module(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) # After auth_provider validated, request select auth module - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "select_mfa_module" step = await manager.login_flow.async_configure( step["flow_id"], {"multi_factor_auth_module": "module2"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" step = await manager.login_flow.async_configure( @@ -825,7 +825,7 @@ async def test_login_with_multi_auth_module(mock_hass): ) # Finally passed, get credential - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert step["result"] assert step["result"].id == "mock-id" @@ -871,13 +871,13 @@ async def test_auth_module_expired_session(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" with patch( @@ -888,7 +888,7 @@ async def test_auth_module_expired_session(mock_hass): step["flow_id"], {"pin": "test-pin"} ) # login flow abort due session timeout - assert step["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step["type"] == data_entry_flow.FlowResultType.ABORT assert step["reason"] == "login_expired" diff --git a/tests/common.py b/tests/common.py index 80f0913cace..acc50e26889 100644 --- a/tests/common.py +++ b/tests/common.py @@ -50,6 +50,7 @@ from homeassistant.helpers import ( entity_platform, entity_registry, intent, + recorder as recorder_helper, restore_state, storage, ) @@ -914,6 +915,8 @@ def init_recorder_component(hass, add_config=None): with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration.migrate_schema" ): + if recorder.DOMAIN not in hass.data: + recorder_helper.async_initialize_recorder(hass) assert setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) assert recorder.DOMAIN in hass.config.components _LOGGER.info( diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index adbea237d34..987a0b74996 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -23,7 +23,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -39,7 +39,7 @@ async def test_one_config_allowed(hass: HomeAssistant) -> None: step_user_result = await flow.async_step_user() - assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step_user_result["type"] == data_entry_flow.FlowResultType.ABORT assert step_user_result["reason"] == "single_instance_allowed" @@ -104,7 +104,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -125,7 +125,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" with patch( @@ -147,7 +147,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: result["flow_id"], user_input={"mfa_code": "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -175,7 +175,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: data=conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch("homeassistant.config_entries.ConfigEntries.async_reload"): @@ -184,7 +184,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: user_input=conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 32bb8bf7c70..9ed2fc82595 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -75,7 +75,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: ), ), patch( "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", - return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + return_value={"type": data_entry_flow.FlowResultType.FORM}, ) as mock_async_step_reauth: await setup_platform(hass, ALARM_DOMAIN) diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index c8f2d3c8c89..b4d804dceae 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -25,7 +25,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -134,7 +134,7 @@ async def test_create_entry(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "abcd" assert result["data"][CONF_NAME] == "abcd" assert result["data"][CONF_LATITUDE] == 55.55 @@ -171,14 +171,14 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FORECAST: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FORECAST: True} await hass.async_block_till_done() diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index c50e0b9971f..4990d196a08 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -47,7 +47,7 @@ async def test_show_form_no_hubs(hass, mock_hub_discover): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" # Check we performed the discovery @@ -66,7 +66,7 @@ async def test_show_form_one_hub(hass, mock_hub_discover, mock_hub_run): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_1.id assert result["result"].data == { CONF_HOST: DUMMY_HOST1, @@ -91,7 +91,7 @@ async def test_show_form_two_hubs(hass, mock_hub_discover): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Check we performed the discovery @@ -117,7 +117,7 @@ async def test_create_second_entry(hass, mock_hub_run, mock_hub_discover): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_2.id assert result["result"].data == { CONF_HOST: DUMMY_HOST2, diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py index 998e4df8b15..0c12c486754 100644 --- a/tests/components/adax/test_config_flow.py +++ b/tests/components/adax/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.components.adax.const import ( ) from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -39,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("adax.get_adax_token", return_value="test_token",), patch( "homeassistant.components.adax.async_setup_entry", @@ -73,7 +73,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch( "adax.get_adax_token", @@ -83,7 +83,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result2["flow_id"], TEST_DATA, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -108,7 +108,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("adax.get_adax_token", return_value="token"): result3 = await hass.config_entries.flow.async_configure( @@ -129,7 +129,7 @@ async def test_local_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -138,7 +138,7 @@ async def test_local_create_entry(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -190,7 +190,7 @@ async def test_local_flow_entry_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -199,7 +199,7 @@ async def test_local_flow_entry_already_exists(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -228,7 +228,7 @@ async def test_local_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -237,7 +237,7 @@ async def test_local_connection_error(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -253,7 +253,7 @@ async def test_local_connection_error(hass): test_data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -263,7 +263,7 @@ async def test_local_heater_not_available(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -272,7 +272,7 @@ async def test_local_heater_not_available(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -288,7 +288,7 @@ async def test_local_heater_not_available(hass): test_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "heater_not_available" @@ -298,7 +298,7 @@ async def test_local_heater_not_found(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -307,7 +307,7 @@ async def test_local_heater_not_found(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -323,7 +323,7 @@ async def test_local_heater_not_found(hass): test_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "heater_not_found" @@ -333,7 +333,7 @@ async def test_local_invalid_wifi_cred(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -342,7 +342,7 @@ async def test_local_invalid_wifi_cred(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -358,5 +358,5 @@ async def test_local_invalid_wifi_cred(hass): test_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_auth" diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index f5b30308a52..4bcfb60e7b6 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -35,7 +35,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_connection_error( ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -78,14 +78,14 @@ async def test_full_flow_implementation( assert result assert result.get("flow_id") - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=FIXTURE_USER_INPUT ) assert result2 - assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2.get("title") == FIXTURE_USER_INPUT[CONF_HOST] data = result2.get("data") @@ -132,7 +132,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -154,7 +154,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -176,14 +176,14 @@ async def test_hassio_confirm( context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "AdGuard Home Addon"} result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2 - assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2.get("title") == "AdGuard Home Addon" data = result2.get("data") @@ -215,6 +215,6 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 275b5fc4e52..4bd792808fb 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -54,7 +54,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac2-filter" # Test First Motion Sensor - entity_id = "binary_sensor.zone_open_with_sensor_motion" + entity_id = "binary_sensor.ac_one_zone_open_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -64,7 +64,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-motion" # Test Second Motion Sensor - entity_id = "binary_sensor.zone_closed_with_sensor_motion" + entity_id = "binary_sensor.ac_one_zone_closed_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -74,7 +74,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-motion" # Test First MyZone Sensor (disabled by default) - entity_id = "binary_sensor.zone_open_with_sensor_myzone" + entity_id = "binary_sensor.ac_one_zone_open_with_sensor_myzone" assert not hass.states.get(entity_id) @@ -96,7 +96,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-myzone" # Test Second Motion Sensor (disabled by default) - entity_id = "binary_sensor.zone_closed_with_sensor_myzone" + entity_id = "binary_sensor.ac_one_zone_closed_with_sensor_myzone" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 4d075a36151..f1b118ab3b3 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -2,14 +2,12 @@ from json import loads from homeassistant.components.advantage_air.climate import ( - ADVANTAGE_AIR_SERVICE_SET_MYZONE, HASS_FAN_MODES, HASS_HVAC_MODES, ) from homeassistant.components.advantage_air.const import ( ADVANTAGE_AIR_STATE_OFF, ADVANTAGE_AIR_STATE_ON, - DOMAIN as ADVANTAGE_AIR_DOMAIN, ) from homeassistant.components.climate.const import ( ATTR_FAN_MODE, @@ -34,7 +32,7 @@ from tests.components.advantage_air import ( async def test_climate_async_setup_entry(hass, aioclient_mock): - """Test climate setup.""" + """Test climate platform.""" aioclient_mock.get( TEST_SYSTEM_URL, @@ -122,7 +120,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test Climate Zone Entity - entity_id = "climate.zone_open_with_sensor" + entity_id = "climate.ac_one_zone_open_with_sensor" state = hass.states.get(entity_id) assert state assert state.attributes.get("min_temp") == 16 @@ -170,21 +168,6 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][0] == "GET" assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" - # Test set_myair service - await hass.services.async_call( - ADVANTAGE_AIR_DOMAIN, - ADVANTAGE_AIR_SERVICE_SET_MYZONE, - {ATTR_ENTITY_ID: [entity_id]}, - blocking=True, - ) - assert len(aioclient_mock.mock_calls) == 17 - assert aioclient_mock.mock_calls[-2][0] == "GET" - assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" - data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) - assert data["ac1"]["info"]["myZone"] == 1 - assert aioclient_mock.mock_calls[-1][0] == "GET" - assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" - async def test_climate_async_failed_update(hass, aioclient_mock): """Test climate change failure.""" diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index a8e219fff89..533cf2a17a4 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass, aioclient_mock): result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["type"] == data_entry_flow.FlowResultType.FORM assert result1["step_id"] == "user" assert result1["errors"] == {} @@ -33,7 +33,7 @@ async def test_form(hass, aioclient_mock): ) assert len(aioclient_mock.mock_calls) == 1 - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "testname" assert result2["data"] == USER_INPUT await hass.async_block_till_done() @@ -47,7 +47,7 @@ async def test_form(hass, aioclient_mock): result3["flow_id"], USER_INPUT, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result4["type"] == data_entry_flow.FlowResultType.ABORT async def test_form_cannot_connect(hass, aioclient_mock): @@ -66,7 +66,7 @@ async def test_form_cannot_connect(hass, aioclient_mock): USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index 2868179b3ee..90cdf9a2168 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -27,7 +27,7 @@ from tests.components.advantage_air import ( async def test_cover_async_setup_entry(hass, aioclient_mock): - """Test climate setup without sensors.""" + """Test cover platform.""" aioclient_mock.get( TEST_SYSTEM_URL, @@ -45,7 +45,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Cover Zone Entity - entity_id = "cover.zone_open_without_sensor" + entity_id = "cover.ac_two_zone_open_without_sensor" state = hass.states.get(entity_id) assert state assert state.state == STATE_OPEN @@ -119,8 +119,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_CLOSE_COVER, { ATTR_ENTITY_ID: [ - "cover.zone_open_without_sensor", - "cover.zone_closed_without_sensor", + "cover.ac_two_zone_open_without_sensor", + "cover.ac_two_zone_closed_without_sensor", ] }, blocking=True, @@ -134,8 +134,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_OPEN_COVER, { ATTR_ENTITY_ID: [ - "cover.zone_open_without_sensor", - "cover.zone_closed_without_sensor", + "cover.ac_two_zone_open_without_sensor", + "cover.ac_two_zone_closed_without_sensor", ] }, blocking=True, diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index 3d246f566e5..20d42fdcffc 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -19,7 +19,7 @@ from tests.components.advantage_air import ( async def test_select_async_setup_entry(hass, aioclient_mock): - """Test climate setup without sensors.""" + """Test select platform.""" aioclient_mock.get( TEST_SYSTEM_URL, @@ -36,7 +36,7 @@ async def test_select_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 - # Test Select Entity + # Test MyZone Select Entity entity_id = "select.ac_one_myzone" state = hass.states.get(entity_id) assert state diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 997f11dea91..4dc2f1baaff 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -91,7 +91,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test First Zone Vent Sensor - entity_id = "sensor.zone_open_with_sensor_vent" + entity_id = "sensor.ac_one_zone_open_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 100 @@ -101,7 +101,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-vent" # Test Second Zone Vent Sensor - entity_id = "sensor.zone_closed_with_sensor_vent" + entity_id = "sensor.ac_one_zone_closed_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 0 @@ -111,7 +111,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-vent" # Test First Zone Signal Sensor - entity_id = "sensor.zone_open_with_sensor_signal" + entity_id = "sensor.ac_one_zone_open_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 40 @@ -121,7 +121,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-signal" # Test Second Zone Signal Sensor - entity_id = "sensor.zone_closed_with_sensor_signal" + entity_id = "sensor.ac_one_zone_closed_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 10 @@ -131,7 +131,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-signal" # Test First Zone Temp Sensor (disabled by default) - entity_id = "sensor.zone_open_with_sensor_temperature" + entity_id = "sensor.ac_one_zone_open_with_sensor_temperature" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py index 1a78025df70..be78edf8ffe 100644 --- a/tests/components/advantage_air/test_switch.py +++ b/tests/components/advantage_air/test_switch.py @@ -23,7 +23,7 @@ from tests.components.advantage_air import ( async def test_cover_async_setup_entry(hass, aioclient_mock): - """Test climate setup without sensors.""" + """Test switch platform.""" aioclient_mock.get( TEST_SYSTEM_URL, diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index af14c170f40..bba177b96d2 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -49,7 +49,7 @@ async def test_form(hass): entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -79,14 +79,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_STATION_UPDATES: False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { CONF_STATION_UPDATES: False, } @@ -97,14 +97,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_STATION_UPDATES: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { CONF_STATION_UPDATES: True, } diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py index 01cb31b3f19..b36044e45b1 100644 --- a/tests/components/agent_dvr/test_config_flow.py +++ b/tests/components/agent_dvr/test_config_flow.py @@ -20,7 +20,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_user_device_exists_abort( @@ -35,7 +35,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "example.local", CONF_PORT: 8090}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_connection_error( @@ -53,7 +53,7 @@ async def test_connection_error( assert result["errors"]["base"] == "cannot_connect" assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_full_user_flow_implementation( @@ -78,7 +78,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "example.local", CONF_PORT: 8090} @@ -88,7 +88,7 @@ async def test_full_user_flow_implementation( assert result["data"][CONF_PORT] == 8090 assert result["data"][SERVER_URL] == "http://example.local:8090/" assert result["title"] == "DESKTOP" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "c0715bba-c2d0-48ef-9e3e-bc81c9ea4447" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 8b593e85cf4..b7c2a65812e 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -26,7 +26,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -84,7 +84,7 @@ async def test_create_entry(hass, aioclient_mock): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -106,7 +106,7 @@ async def test_create_entry_with_nearest_method(hass, aioclient_mock): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index b26775d7051..02236e826e5 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -72,7 +72,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch("pyairnow.WebServiceAPI._get", return_value=MOCK_RESPONSE), patch( @@ -86,7 +86,7 @@ async def test_form(hass): await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index 0ecb2c7a8dc..6e3a1579f70 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -6,7 +6,7 @@ import airthings from homeassistant import config_entries from homeassistant.components.airthings.const import CONF_ID, CONF_SECRET, DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("airthings.get_token", return_value="test_token",), patch( @@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Airthings" assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -56,7 +56,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -75,7 +75,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -94,7 +94,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 9f9deca384c..f97ee845dba 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -68,7 +68,7 @@ async def test_duplicate_error(hass, config, config_entry, data): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -150,7 +150,7 @@ async def test_errors(hass, data, exc, errors, integration_type): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == errors @@ -207,14 +207,14 @@ async def test_options_flow(hass, config_entry): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} @@ -229,7 +229,7 @@ async def test_step_geography_by_coords(hass, config, setup_airvisual): result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API (51.528308, -0.3817765)" assert result["data"] == { CONF_API_KEY: "abcde12345", @@ -261,7 +261,7 @@ async def test_step_geography_by_name(hass, config, setup_airvisual): result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API (Beijing, Beijing, China)" assert result["data"] == { CONF_API_KEY: "abcde12345", @@ -289,7 +289,7 @@ async def test_step_node_pro(hass, config, setup_airvisual): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Node/Pro (192.168.1.100)" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -306,7 +306,7 @@ async def test_step_reauth(hass, config_entry, setup_airvisual): assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_api_key = "defgh67890" @@ -317,7 +317,7 @@ async def test_step_reauth(hass, config_entry, setup_airvisual): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: new_api_key} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -330,7 +330,7 @@ async def test_step_user(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -339,7 +339,7 @@ async def test_step_user(hass): data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "geography_by_coords" result = await hass.config_entries.flow.async_init( @@ -348,7 +348,7 @@ async def test_step_user(hass): data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "geography_by_name" result = await hass.config_entries.flow.async_init( @@ -357,5 +357,5 @@ async def test_step_user(hass): data={"type": INTEGRATION_TYPE_NODE_PRO}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "node_pro" diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index 251dcf01b60..32eaade93ee 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -55,7 +55,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}" assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] assert result["data"][CONF_PORT] == CONFIG[CONF_PORT] @@ -84,7 +84,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {CONF_ID: "invalid_system_id"} @@ -95,7 +95,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: result["flow_id"], CONFIG_ID1 ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -103,7 +103,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result["title"] == f"Airzone {CONFIG_ID1[CONF_HOST]}:{CONFIG_ID1[CONF_PORT]}" diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 33117c64110..19017e79570 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant import config_entries from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +18,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -40,7 +36,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Aladdin Connect" assert result2["data"] == { CONF_USERNAME: "test-username", @@ -70,7 +66,7 @@ async def test_form_failed_auth( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -94,7 +90,7 @@ async def test_form_connection_timeout( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -153,7 +149,7 @@ async def test_import_flow_success( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Aladdin Connect" assert result2["data"] == { CONF_USERNAME: "test-user", @@ -185,7 +181,7 @@ async def test_reauth_flow( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -201,7 +197,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { CONF_USERNAME: "test-username", @@ -232,7 +228,7 @@ async def test_reauth_flow_auth_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.return_value = False with patch( @@ -251,7 +247,7 @@ async def test_reauth_flow_auth_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -278,7 +274,7 @@ async def test_reauth_flow_connnection_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.side_effect = ClientConnectionError @@ -292,5 +288,5 @@ async def test_reauth_flow_connnection_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index 4c422ae29ba..4cbb2333cc5 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -9,14 +9,14 @@ from homeassistant.core import HomeAssistant from tests.common import AsyncMock, MockConfigEntry -YAML_CONFIG = {"username": "test-user", "password": "test-password"} +CONFIG = {"username": "test-user", "password": "test-password"} async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: """Test component setup Get Doors Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -38,7 +38,7 @@ async def test_setup_login_error( """Test component setup Login Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -57,7 +57,7 @@ async def test_setup_connection_error( """Test component setup Login Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -74,7 +74,7 @@ async def test_setup_component_no_error(hass: HomeAssistant) -> None: """Test component setup No Error.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -116,7 +116,7 @@ async def test_load_and_unload( """Test loading and unloading Aladdin Connect entry.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) diff --git a/tests/components/aladdin_connect/test_sensor.py b/tests/components/aladdin_connect/test_sensor.py new file mode 100644 index 00000000000..3702bcd9efa --- /dev/null +++ b/tests/components/aladdin_connect/test_sensor.py @@ -0,0 +1,85 @@ +"""Test the Aladdin Connect Sensors.""" +from datetime import timedelta +from unittest.mock import MagicMock, patch + +from homeassistant.components.aladdin_connect.const import DOMAIN +from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, async_fire_time_changed + +CONFIG = {"username": "test-user", "password": "test-password"} +RELOAD_AFTER_UPDATE_DELAY = timedelta(seconds=31) + + +async def test_sensors( + hass: HomeAssistant, + mock_aladdinconnect_api: MagicMock, +) -> None: + """Test Sensors for AladdinConnect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + + await hass.async_block_till_done() + + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entry = registry.async_get("sensor.home_battery_level") + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get("sensor.home_battery_level") + assert state is None + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.home_battery_level") + assert state + + entry = registry.async_get("sensor.home_wi_fi_rssi") + await hass.async_block_till_done() + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state is None + + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index 8a2aae48f9b..e3fdb05ca00 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -62,7 +62,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -70,7 +70,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title): {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" with patch("homeassistant.components.alarmdecoder.config_flow.AdExt.open"), patch( @@ -82,7 +82,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == { **connection, @@ -105,7 +105,7 @@ async def test_setup_connection_error(hass: HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -113,7 +113,7 @@ async def test_setup_connection_error(hass: HomeAssistant): {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" with patch( @@ -123,7 +123,7 @@ async def test_setup_connection_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -133,7 +133,7 @@ async def test_setup_connection_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -152,7 +152,7 @@ async def test_options_arm_flow(hass: HomeAssistant): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -160,7 +160,7 @@ async def test_options_arm_flow(hass: HomeAssistant): user_input={"edit_selection": "Arming Settings"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "arm_settings" with patch( @@ -171,7 +171,7 @@ async def test_options_arm_flow(hass: HomeAssistant): user_input=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: user_input, OPTIONS_ZONES: DEFAULT_ZONE_OPTIONS, @@ -190,7 +190,7 @@ async def test_options_zone_flow(hass: HomeAssistant): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -198,7 +198,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -214,7 +214,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input=zone_settings, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {zone_number: zone_settings}, @@ -223,7 +223,7 @@ async def test_options_zone_flow(hass: HomeAssistant): # Make sure zone can be removed... result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -231,7 +231,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -247,7 +247,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {}, @@ -266,7 +266,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -274,7 +274,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" # Zone Number must be int @@ -283,7 +283,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={CONF_ZONE_NUMBER: "asd"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" assert result["errors"] == {CONF_ZONE_NUMBER: "int"} @@ -292,7 +292,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={CONF_ZONE_NUMBER: zone_number}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive @@ -301,7 +301,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_RELAY_ADDR: "1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -310,7 +310,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_RELAY_CHAN: "1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -320,7 +320,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_RELAY_ADDR: "abc", CONF_RELAY_CHAN: "abc"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == { CONF_RELAY_ADDR: "int", @@ -333,7 +333,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_ZONE_LOOP: "1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_rfid"} @@ -343,7 +343,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "ab"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "int"} @@ -353,7 +353,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "5"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_range"} @@ -372,7 +372,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: { @@ -420,7 +420,7 @@ async def test_one_device_allowed(hass, protocol, connection): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -428,11 +428,11 @@ async def test_one_device_allowed(hass, protocol, connection): {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index 32f49cff043..3bf2db14b95 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -57,7 +57,7 @@ async def test_hassio(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "hassio_confirm" with patch( @@ -67,7 +67,7 @@ async def test_hassio(hass): assert len(mock_setup.mock_calls) == 1 - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.async_entries(DOMAIN)) == 1 entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -83,15 +83,15 @@ async def test_abort_if_existing_entry(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await flow.async_step_import({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await flow.async_step_hassio(HassioServiceInfo(config={})) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -123,7 +123,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://almond.stanford.edu/me/api/oauth2/authorize" f"?response_type=code&client_id={CLIENT_ID_VALUE}" diff --git a/tests/components/ambee/test_config_flow.py b/tests/components/ambee/test_config_flow.py index a6220418681..233bbaf232b 100644 --- a/tests/components/ambee/test_config_flow.py +++ b/tests/components/ambee/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.ambee.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +19,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -42,7 +38,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_API_KEY: "example", @@ -64,7 +60,7 @@ async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -82,7 +78,7 @@ async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_api_key"} assert "flow_id" in result2 @@ -102,7 +98,7 @@ async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Name" assert result3.get("data") == { CONF_API_KEY: "example", @@ -131,7 +127,7 @@ async def test_api_error(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} @@ -150,7 +146,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -165,7 +161,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_API_KEY: "other_key", @@ -196,7 +192,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -211,7 +207,7 @@ async def test_reauth_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_api_key"} assert "flow_id" in result2 @@ -227,7 +223,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("type") == FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_API_KEY: "other_key", @@ -267,6 +263,6 @@ async def test_reauth_api_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/ambee/test_init.py b/tests/components/ambee/test_init.py index c6ad45735ff..059c58da803 100644 --- a/tests/components/ambee/test_init.py +++ b/tests/components/ambee/test_init.py @@ -1,6 +1,8 @@ """Tests for the Ambee integration.""" +from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock, MagicMock, patch +from aiohttp import ClientWebSocketResponse from ambee import AmbeeConnectionError from ambee.exceptions import AmbeeAuthenticationError import pytest @@ -10,12 +12,14 @@ from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +from tests.components.repairs import get_repairs async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ambee: AsyncMock, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], ) -> None: """Test the Ambee configuration entry loading/unloading.""" mock_config_entry.add_to_hass(hass) @@ -24,9 +28,16 @@ async def test_load_unload_config_entry( assert mock_config_entry.state is ConfigEntryState.LOADED + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "pending_removal" + await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 0 + assert not hass.data.get(DOMAIN) diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py index a32398139d0..d143aea8f7c 100644 --- a/tests/components/ambee/test_sensor.py +++ b/tests/components/ambee/test_sensor.py @@ -41,7 +41,10 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_2_5" assert state.state == "3.14" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Air quality Particulate matter < 2.5 μm" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -56,7 +59,10 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_10" assert state.state == "5.24" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Air quality Particulate matter < 10 μm" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -71,7 +77,9 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_sulphur_dioxide" assert state.state == "0.031" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Sulphur dioxide (SO2)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -86,7 +94,9 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_nitrogen_dioxide" assert state.state == "0.66" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Nitrogen dioxide (NO2)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -101,7 +111,7 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_ozone" assert state.state == "17.067" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Ozone" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -117,7 +127,9 @@ async def test_air_quality( assert entry.unique_id == f"{entry_id}_air_quality_carbon_monoxide" assert state.state == "0.105" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Carbon Monoxide (CO)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Carbon monoxide (CO)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -131,7 +143,10 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_air_quality_index" assert state.state == "13" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Air quality Air quality index (AQI)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -142,7 +157,7 @@ async def test_air_quality( assert device_entry assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_air_quality")} assert device_entry.manufacturer == "Ambee" - assert device_entry.name == "Air Quality" + assert device_entry.name == "Air quality" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert not device_entry.model assert not device_entry.sw_version @@ -163,7 +178,7 @@ async def test_pollen( assert state assert entry.unique_id == f"{entry_id}_pollen_grass" assert state.state == "190" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Grass" assert state.attributes.get(ATTR_ICON) == "mdi:grass" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( @@ -178,7 +193,7 @@ async def test_pollen( assert state assert entry.unique_id == f"{entry_id}_pollen_tree" assert state.state == "127" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Tree" assert state.attributes.get(ATTR_ICON) == "mdi:tree" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( @@ -193,7 +208,7 @@ async def test_pollen( assert state assert entry.unique_id == f"{entry_id}_pollen_weed" assert state.state == "95" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Weed" assert state.attributes.get(ATTR_ICON) == "mdi:sprout" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( @@ -209,7 +224,7 @@ async def test_pollen( assert entry.unique_id == f"{entry_id}_pollen_grass_risk" assert state.state == "high" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen Risk" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Grass risk" assert state.attributes.get(ATTR_ICON) == "mdi:grass" assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -221,7 +236,7 @@ async def test_pollen( assert entry.unique_id == f"{entry_id}_pollen_tree_risk" assert state.state == "moderate" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen Risk" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Tree risk" assert state.attributes.get(ATTR_ICON) == "mdi:tree" assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -233,7 +248,7 @@ async def test_pollen( assert entry.unique_id == f"{entry_id}_pollen_weed_risk" assert state.state == "high" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen Risk" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Weed risk" assert state.attributes.get(ATTR_ICON) == "mdi:sprout" assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -286,20 +301,20 @@ async def test_pollen_disabled_by_default( @pytest.mark.parametrize( "key,icon,name,value", [ - ("grass_poaceae", "mdi:grass", "Poaceae Grass Pollen", "190"), - ("tree_alder", "mdi:tree", "Alder Tree Pollen", "0"), - ("tree_birch", "mdi:tree", "Birch Tree Pollen", "35"), - ("tree_cypress", "mdi:tree", "Cypress Tree Pollen", "0"), - ("tree_elm", "mdi:tree", "Elm Tree Pollen", "0"), - ("tree_hazel", "mdi:tree", "Hazel Tree Pollen", "0"), - ("tree_oak", "mdi:tree", "Oak Tree Pollen", "55"), - ("tree_pine", "mdi:tree", "Pine Tree Pollen", "30"), - ("tree_plane", "mdi:tree", "Plane Tree Pollen", "5"), - ("tree_poplar", "mdi:tree", "Poplar Tree Pollen", "0"), - ("weed_chenopod", "mdi:sprout", "Chenopod Weed Pollen", "0"), - ("weed_mugwort", "mdi:sprout", "Mugwort Weed Pollen", "1"), - ("weed_nettle", "mdi:sprout", "Nettle Weed Pollen", "88"), - ("weed_ragweed", "mdi:sprout", "Ragweed Weed Pollen", "3"), + ("grass_poaceae", "mdi:grass", "Poaceae grass", "190"), + ("tree_alder", "mdi:tree", "Alder tree", "0"), + ("tree_birch", "mdi:tree", "Birch tree", "35"), + ("tree_cypress", "mdi:tree", "Cypress tree", "0"), + ("tree_elm", "mdi:tree", "Elm tree", "0"), + ("tree_hazel", "mdi:tree", "Hazel tree", "0"), + ("tree_oak", "mdi:tree", "Oak tree", "55"), + ("tree_pine", "mdi:tree", "Pine tree", "30"), + ("tree_plane", "mdi:tree", "Plane tree", "5"), + ("tree_poplar", "mdi:tree", "Poplar tree", "0"), + ("weed_chenopod", "mdi:sprout", "Chenopod weed", "0"), + ("weed_mugwort", "mdi:sprout", "Mugwort weed", "1"), + ("weed_nettle", "mdi:sprout", "Nettle weed", "88"), + ("weed_ragweed", "mdi:sprout", "Ragweed weed", "3"), ], ) async def test_pollen_enable_disable_by_defaults( @@ -335,7 +350,7 @@ async def test_pollen_enable_disable_by_defaults( assert state assert entry.unique_id == f"{entry_id}_pollen_{key}" assert state.state == value - assert state.attributes.get(ATTR_FRIENDLY_NAME) == name + assert state.attributes.get(ATTR_FRIENDLY_NAME) == f"Pollen {name}" assert state.attributes.get(ATTR_ICON) == icon assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( diff --git a/tests/components/amberelectric/test_config_flow.py b/tests/components/amberelectric/test_config_flow.py index ce474be1b3d..2be77f19bf1 100644 --- a/tests/components/amberelectric/test_config_flow.py +++ b/tests/components/amberelectric/test_config_flow.py @@ -67,7 +67,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: initial_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert initial_result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM assert initial_result.get("step_id") == "user" # Test filling in API key @@ -76,7 +76,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: API_KEY}, ) - assert enter_api_key_result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM assert enter_api_key_result.get("step_id") == "site" select_site_result = await hass.config_entries.flow.async_configure( @@ -85,7 +85,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: ) # Show available sites - assert select_site_result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert select_site_result.get("title") == "Home" data = select_site_result.get("data") assert data @@ -102,7 +102,7 @@ async def test_no_site(hass: HomeAssistant, no_site_api: Mock) -> None: data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "no_site"} @@ -113,7 +113,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -122,7 +122,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "invalid_api_token"} @@ -133,7 +133,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -142,7 +142,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "unknown_error"} diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 2da550afd42..e2c8e46dc46 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -35,7 +35,7 @@ async def test_abort_if_no_implementation_registered(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -48,12 +48,12 @@ async def test_abort_if_already_setup(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -63,7 +63,7 @@ async def test_full_flow_implementation(hass): flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert ( result["description_placeholders"]["cb_url"] @@ -78,7 +78,7 @@ async def test_full_flow_implementation(hass): with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value="test"): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Ambiclimate" assert result["data"]["callback_url"] == "https://example.com/api/ambiclimate" assert result["data"][CONF_CLIENT_SECRET] == "secret" @@ -86,14 +86,14 @@ async def test_full_flow_implementation(hass): with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT with patch( "ambiclimate.AmbiclimateOAuth.get_access_token", side_effect=ambiclimate.AmbiclimateOauthError(), ): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_abort_invalid_code(hass): @@ -103,7 +103,7 @@ async def test_abort_invalid_code(hass): with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("invalid") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "access_token" @@ -115,7 +115,7 @@ async def test_already_setup(hass): context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index a72534b8478..0e298c40c0e 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -15,7 +15,7 @@ async def test_duplicate_error(hass, config, config_entry, setup_ambient_station result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -31,7 +31,7 @@ async def test_errors(hass, config, devices, error, setup_ambient_station): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error} @@ -40,7 +40,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -49,7 +49,7 @@ async def test_step_user(hass, config, setup_ambient_station): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "67890fghij67" assert result["data"] == { CONF_API_KEY: "12345abcde12345abcde", diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index a18c59f171f..82a61126432 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -269,8 +269,8 @@ async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_moc hass.config.components = ["default_config"] with patch( - "homeassistant.components.analytics.analytics.async_get_integration", - side_effect=IntegrationNotFound("any"), + "homeassistant.components.analytics.analytics.async_get_integrations", + return_value={"any": IntegrationNotFound("any")}, ), patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() @@ -291,8 +291,8 @@ async def test_send_statistics_async_get_integration_unknown_exception( hass.config.components = ["default_config"] with pytest.raises(ValueError), patch( - "homeassistant.components.analytics.analytics.async_get_integration", - side_effect=ValueError, + "homeassistant.components.analytics.analytics.async_get_integrations", + return_value={"any": ValueError()}, ), patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index d5301f7ada3..a0fb86eb803 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -97,7 +97,7 @@ async def test_user(hass, config, eth_mac, wifi_mac): flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow_result["type"] == data_entry_flow.FlowResultType.FORM assert flow_result["step_id"] == "user" # test with all provided @@ -110,7 +110,7 @@ async def test_user(hass, config, eth_mac, wifi_mac): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config @@ -134,7 +134,7 @@ async def test_user_adbkey(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config_data @@ -152,7 +152,7 @@ async def test_error_both_key_server(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "key_and_server"} with patch( @@ -164,7 +164,7 @@ async def test_error_both_key_server(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -179,7 +179,7 @@ async def test_error_invalid_key(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "adbkey_not_file"} with patch( @@ -191,7 +191,7 @@ async def test_error_invalid_key(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -219,7 +219,7 @@ async def test_invalid_mac(hass, config, eth_mac, wifi_mac): data=config, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -237,7 +237,7 @@ async def test_abort_if_host_exist(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -260,7 +260,7 @@ async def test_abort_if_unique_exist(hass): data=CONFIG_ADB_SERVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -275,7 +275,7 @@ async def test_on_connect_failed(hass): result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -285,7 +285,7 @@ async def test_on_connect_failed(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} with patch( @@ -297,7 +297,7 @@ async def test_on_connect_failed(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == HOST assert result3["data"] == CONFIG_ADB_SERVER @@ -320,7 +320,7 @@ async def test_options_flow(hass): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test app form with existing app @@ -330,7 +330,7 @@ async def test_options_flow(hass): CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "apps" # test change value in apps form @@ -340,7 +340,7 @@ async def test_options_flow(hass): CONF_APP_NAME: "Appl1", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test app form with new app @@ -350,7 +350,7 @@ async def test_options_flow(hass): CONF_APPS: APPS_NEW_ID, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "apps" # test save value for new app @@ -361,7 +361,7 @@ async def test_options_flow(hass): CONF_APP_NAME: "Appl2", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test app form for delete @@ -371,7 +371,7 @@ async def test_options_flow(hass): CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "apps" # test delete app1 @@ -382,7 +382,7 @@ async def test_options_flow(hass): CONF_APP_DELETE: True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test rules form with existing rule @@ -392,7 +392,7 @@ async def test_options_flow(hass): CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" # test change value in rule form with invalid json rule @@ -402,7 +402,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: "a", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -413,7 +413,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: json.dumps({"a": "b"}), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -424,7 +424,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: json.dumps(["standby"]), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test rule form with new rule @@ -434,7 +434,7 @@ async def test_options_flow(hass): CONF_STATE_DETECTION_RULES: RULES_NEW_ID, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" # test save value for new rule @@ -445,7 +445,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: json.dumps(VALID_DETECT_RULE), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test rules form with delete existing rule @@ -455,7 +455,7 @@ async def test_options_flow(hass): CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" # test delete rule @@ -465,7 +465,7 @@ async def test_options_flow(hass): CONF_RULE_DELETE: True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -479,7 +479,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY apps_options = config_entry.options[CONF_APPS] assert apps_options.get("app1") is None diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 73f8de55cc9..f5487c78425 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,5 +1,4 @@ """The tests for the androidtv platform.""" -import base64 import logging from unittest.mock import Mock, patch @@ -52,7 +51,6 @@ from homeassistant.components.media_player import ( SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, ) -from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_COMMAND, @@ -851,10 +849,10 @@ async def test_androidtv_volume_set(hass): patch_set_volume_level.assert_called_with(0.5) -async def test_get_image(hass, hass_ws_client): +async def test_get_image_http(hass, hass_client_no_auth): """Test taking a screen capture. - This is based on `test_get_image` in tests/components/media_player/test_init.py. + This is based on `test_get_image_http` in tests/components/media_player/test_init.py. """ patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) @@ -868,44 +866,36 @@ async def test_get_image(hass, hass_ws_client): with patchers.patch_shell("11")[patch_key]: await async_update_entity(hass, entity_id) - client = await hass_ws_client(hass) + media_player_name = "media_player." + slugify( + CONFIG_ANDROIDTV_DEFAULT[TEST_ENTITY_NAME] + ) + state = hass.states.get(media_player_name) + assert "entity_picture_local" not in state.attributes + + client = await hass_client_no_auth() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", return_value=b"image" ): - await client.send_json( - {"id": 5, "type": "media_player_thumbnail", "entity_id": entity_id} - ) + resp = await client.get(state.attributes["entity_picture"]) + content = await resp.read() - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"]["content_type"] == "image/png" - assert msg["result"]["content"] == base64.b64encode(b"image").decode("utf-8") + assert content == b"image" with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", side_effect=ConnectionResetError, ): - await client.send_json( - {"id": 6, "type": "media_player_thumbnail", "entity_id": entity_id} - ) + resp = await client.get(state.attributes["entity_picture"]) - msg = await client.receive_json() - - # The device is unavailable, but getting the media image did not cause an exception - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_UNAVAILABLE + # The device is unavailable, but getting the media image did not cause an exception + state = hass.states.get(media_player_name) + assert state is not None + assert state.state == STATE_UNAVAILABLE -async def test_get_image_disabled(hass, hass_ws_client): - """Test taking a screen capture with screencap option disabled. - - This is based on `test_get_image` in tests/components/media_player/test_init.py. - """ +async def test_get_image_disabled(hass): + """Test that the screencap option can disable entity_picture.""" patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( @@ -921,17 +911,12 @@ async def test_get_image_disabled(hass, hass_ws_client): with patchers.patch_shell("11")[patch_key]: await async_update_entity(hass, entity_id) - client = await hass_ws_client(hass) - - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", return_value=b"image" - ) as screen_cap: - await client.send_json( - {"id": 5, "type": "media_player_thumbnail", "entity_id": entity_id} - ) - - await client.receive_json() - assert not screen_cap.called + media_player_name = "media_player." + slugify( + CONFIG_ANDROIDTV_DEFAULT[TEST_ENTITY_NAME] + ) + state = hass.states.get(media_player_name) + assert "entity_picture_local" not in state.attributes + assert "entity_picture" not in state.attributes async def _test_service( diff --git a/tests/components/anthemav/__init__.py b/tests/components/anthemav/__init__.py new file mode 100644 index 00000000000..829f99b10b5 --- /dev/null +++ b/tests/components/anthemav/__init__.py @@ -0,0 +1 @@ +"""Tests for the Anthem A/V Receivers integration.""" diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py new file mode 100644 index 00000000000..8fbdf3145c3 --- /dev/null +++ b/tests/components/anthemav/conftest.py @@ -0,0 +1,28 @@ +"""Fixtures for anthemav integration tests.""" +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + + +@pytest.fixture +def mock_anthemav() -> AsyncMock: + """Return the default mocked anthemav.""" + avr = AsyncMock() + avr.protocol.macaddress = "000000000001" + avr.protocol.model = "MRX 520" + avr.reconnect = AsyncMock() + avr.close = MagicMock() + avr.protocol.input_list = [] + avr.protocol.audio_listening_mode_list = [] + return avr + + +@pytest.fixture +def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock: + """Return the default mocked connection.create.""" + + with patch( + "anthemav.Connection.create", + return_value=mock_anthemav, + ) as mock: + yield mock diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py new file mode 100644 index 00000000000..1f3dec8d5e1 --- /dev/null +++ b/tests/components/anthemav/test_config_flow.py @@ -0,0 +1,115 @@ +"""Test the Anthem A/V Receivers config flow.""" +from unittest.mock import AsyncMock, patch + +from anthemav.device_error import DeviceError + +from homeassistant import config_entries +from homeassistant.components.anthemav.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_form_with_valid_connection( + hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.anthemav.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 14999, + }, + ) + + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == { + "host": "1.1.1.1", + "port": 14999, + "name": "Anthem AV", + "mac": "00:00:00:00:00:01", + "model": "MRX 520", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_device_info_error(hass: HomeAssistant) -> None: + """Test we handle DeviceError from library.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "anthemav.Connection.create", + side_effect=DeviceError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 14999, + }, + ) + + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_receive_deviceinfo"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "anthemav.Connection.create", + side_effect=OSError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 14999, + }, + ) + + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_import_configuration( + hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock +) -> None: + """Test we import existing configuration.""" + config = { + "host": "1.1.1.1", + "port": 14999, + "name": "Anthem Av Import", + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "host": "1.1.1.1", + "port": 14999, + "name": "Anthem Av Import", + "mac": "00:00:00:00:00:01", + "model": "MRX 520", + } diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py new file mode 100644 index 00000000000..866925f4e46 --- /dev/null +++ b/tests/components/anthemav/test_init.py @@ -0,0 +1,65 @@ +"""Test the Anthem A/V Receivers config flow.""" +from unittest.mock import ANY, AsyncMock, patch + +from homeassistant import config_entries +from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock +) -> None: + """Test load and unload AnthemAv component.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 14999, + CONF_NAME: "Anthem AV", + CONF_MAC: "aabbccddeeff", + CONF_MODEL: "MRX 520", + }, + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # assert avr is created + mock_connection_create.assert_called_with( + host="1.1.1.1", port=14999, update_callback=ANY + ) + assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + + # unload + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + # assert unload and avr is closed + assert mock_config_entry.state == config_entries.ConfigEntryState.NOT_LOADED + mock_anthemav.close.assert_called_once() + + +async def test_config_entry_not_ready(hass: HomeAssistant) -> None: + """Test AnthemAV configuration entry not ready.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 14999, + CONF_NAME: "Anthem AV", + CONF_MAC: "aabbccddeeff", + CONF_MODEL: "MRX 520", + }, + ) + + with patch( + "anthemav.Connection.create", + side_effect=OSError, + ): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 6efb4820564..862019b529e 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -71,7 +71,7 @@ async def test_user_input_device_not_found(hass, mrp_device): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -79,7 +79,7 @@ async def test_user_input_device_not_found(hass, mrp_device): {"device_input": "none"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "no_devices_found"} @@ -95,7 +95,7 @@ async def test_user_input_unexpected_error(hass, mock_scan): {"device_input": "dummy"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -104,31 +104,31 @@ async def test_user_adds_full_device(hass, full_device, pairing): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"device_input": "MRP Device"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["description_placeholders"] == {"protocol": "MRP"} result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["description_placeholders"] == {"protocol": "DMAP", "pin": 1111} result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result5["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result5["type"] == data_entry_flow.FlowResultType.FORM assert result5["description_placeholders"] == {"protocol": "AirPlay"} result6 = await hass.config_entries.flow.async_configure( @@ -157,14 +157,14 @@ async def test_user_adds_dmap_device(hass, dmap_device, dmap_pin, pairing): result["flow_id"], {"device_input": "DMAP Device"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["description_placeholders"] == {"pin": 1111, "protocol": "DMAP"} result6 = await hass.config_entries.flow.async_configure( @@ -195,7 +195,7 @@ async def test_user_adds_dmap_device_failed(hass, dmap_device, dmap_pin, pairing await hass.config_entries.flow.async_configure(result["flow_id"], {}) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "device_did_not_pair" @@ -211,7 +211,7 @@ async def test_user_adds_device_with_ip_filter( result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -268,7 +268,7 @@ async def test_user_adds_existing_device(hass, mrp_device): result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} @@ -294,7 +294,7 @@ async def test_user_connection_failed(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -315,7 +315,7 @@ async def test_user_start_pair_error_failed(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "invalid_auth" @@ -336,14 +336,14 @@ async def test_user_pair_service_with_password( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "password" result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "setup_failed" @@ -363,14 +363,14 @@ async def test_user_pair_disabled_service(hass, dmap_with_requirement, pairing_m result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol_disabled" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -390,7 +390,7 @@ async def test_user_pair_ignore_unsupported(hass, dmap_with_requirement, pairing result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "setup_failed" @@ -416,7 +416,7 @@ async def test_user_pair_invalid_pin(hass, mrp_device, pairing_mock): result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -442,7 +442,7 @@ async def test_user_pair_unexpected_error(hass, mrp_device, pairing_mock): result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -463,7 +463,7 @@ async def test_user_pair_backoff_error(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "backoff" @@ -484,7 +484,7 @@ async def test_user_pair_begin_unexpected_error(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "unknown" @@ -499,14 +499,14 @@ async def test_ignores_disabled_service(hass, airplay_with_disabled_mrp, pairing result["flow_id"], {"device_input": "mrpid"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == { "name": "AirPlay Device", "type": "Unknown", } result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "AirPlay"} result3 = await hass.config_entries.flow.async_configure( @@ -541,7 +541,7 @@ async def test_zeroconf_unsupported_service_aborts(hass): properties={}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" @@ -560,7 +560,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): type="_mediaremotetv._tcp.local.", ), ) - assert unrelated_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert unrelated_result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, @@ -575,7 +575,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): type="_mediaremotetv._tcp.local.", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", @@ -585,7 +585,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( @@ -605,7 +605,7 @@ async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -615,7 +615,7 @@ async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "DMAP", "pin": 1111} result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -654,7 +654,7 @@ async def test_zeroconf_ip_change(hass, mock_scan): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -693,7 +693,7 @@ async def test_zeroconf_ip_change_via_secondary_identifier(hass, mock_scan): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -709,7 +709,7 @@ async def test_zeroconf_add_existing_aborts(hass, dmap_device): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -718,7 +718,7 @@ async def test_zeroconf_add_but_device_not_found(hass, mock_scan): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -729,7 +729,7 @@ async def test_zeroconf_add_existing_device(hass, dmap_device): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -740,7 +740,7 @@ async def test_zeroconf_unexpected_error(hass, mock_scan): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" @@ -764,7 +764,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" mock_scan.result = [ @@ -786,7 +786,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, ), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -844,7 +844,7 @@ async def test_zeroconf_missing_device_during_protocol_resolve( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "device_not_found" @@ -904,7 +904,7 @@ async def test_zeroconf_additional_protocol_resolve_failure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "inconsistent_device" @@ -930,7 +930,7 @@ async def test_zeroconf_pair_additionally_found_protocols( properties={"deviceid": "airplayid"}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM await hass.async_block_till_done() mock_scan.result = [ @@ -981,7 +981,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pair_no_pin" assert result2["description_placeholders"] == {"pin": ANY, "protocol": "RAOP"} @@ -991,7 +991,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "pair_with_pin" assert result3["description_placeholders"] == {"protocol": "MRP"} @@ -999,7 +999,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["step_id"] == "pair_with_pin" assert result4["description_placeholders"] == {"protocol": "AirPlay"} @@ -1007,7 +1007,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result5["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Re-configuration @@ -1030,13 +1030,13 @@ async def test_reconfigure_update_credentials(hass, mrp_device, pairing): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert config_entry.data == { @@ -1058,7 +1058,7 @@ async def test_option_start_off(hass): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_START_OFF: True} @@ -1083,5 +1083,5 @@ async def test_zeroconf_rejects_ipv6(hass): properties={"CtlN": "Apple TV"}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index dd5995a8f4f..3b7a414305f 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -160,7 +160,7 @@ class OAuthFixture: ) result = await self.hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == self.title assert "data" in result assert "token" in result["data"] @@ -417,7 +417,7 @@ async def test_config_flow_no_credentials(hass): result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -444,7 +444,7 @@ async def test_config_flow_other_domain( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -467,7 +467,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP result = await oauth_fixture.complete_external_step(result) assert ( result["data"].get("auth_implementation") == "fake_integration_some_client_id" @@ -511,14 +511,14 @@ async def test_config_flow_multiple_entries( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "pick_implementation" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"implementation": "fake_integration_some_client_id2"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.client_id = CLIENT_ID + "2" oauth_fixture.title = CLIENT_ID + "2" result = await oauth_fixture.complete_external_step(result) @@ -548,7 +548,7 @@ async def test_config_flow_create_delete_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -565,7 +565,7 @@ async def test_config_flow_with_config_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -583,7 +583,7 @@ async def test_import_without_setup(hass, config_credential): result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -612,7 +612,7 @@ async def test_websocket_without_platform( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -687,7 +687,7 @@ async def test_platform_with_auth_implementation( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -745,7 +745,7 @@ async def test_name( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.title = NAME result = await oauth_fixture.complete_external_step(result) assert ( diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index efcbb0b691c..eeb5adbc7e3 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -65,11 +65,11 @@ async def test_ssdp(hass, dummy_client): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY @@ -86,7 +86,7 @@ async def test_ssdp_abort(hass): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -99,11 +99,11 @@ async def test_ssdp_unable_to_connect(hass, dummy_client): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -122,7 +122,7 @@ async def test_ssdp_update(hass): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == MOCK_HOST @@ -137,7 +137,7 @@ async def test_user(hass, aioclient_mock): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -149,7 +149,7 @@ async def test_user(hass, aioclient_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id == MOCK_UUID @@ -168,7 +168,7 @@ async def test_invalid_ssdp(hass, aioclient_mock): context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id is None @@ -187,7 +187,7 @@ async def test_user_wrong(hass, aioclient_mock): context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["result"].unique_id is None diff --git a/tests/components/aseko_pool_live/test_config_flow.py b/tests/components/aseko_pool_live/test_config_flow.py index 5ab85c61a8b..754c7fd0a3c 100644 --- a/tests/components/aseko_pool_live/test_config_flow.py +++ b/tests/components/aseko_pool_live/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries, setup from homeassistant.components.aseko_pool_live.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "aseko@example.com" assert result2["data"] == {CONF_ACCESS_TOKEN: "any_access_token"} assert len(mock_setup_entry.mock_calls) == 1 @@ -82,5 +82,5 @@ async def test_get_account_info_exceptions( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": reason} diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 8475bd48f9a..013a81e7184 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -81,7 +81,7 @@ async def test_user(hass, mock_unique_id, unique_id): flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow_result["type"] == data_entry_flow.FlowResultType.FORM assert flow_result["step_id"] == "user" # test with all provided @@ -92,7 +92,7 @@ async def test_user(hass, mock_unique_id, unique_id): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA @@ -116,7 +116,7 @@ async def test_error_wrong_password_ssh(hass, config, error): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error} @@ -136,7 +136,7 @@ async def test_error_invalid_ssh(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "ssh_not_file"} @@ -152,7 +152,7 @@ async def test_error_invalid_host(hass): data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_host"} @@ -168,7 +168,7 @@ async def test_abort_if_not_unique_id_setup(hass): context={"source": SOURCE_USER}, data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_unique_id" @@ -192,7 +192,7 @@ async def test_update_uniqueid_exist(hass, mock_unique_id): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA prev_entry = hass.config_entries.async_get_entry(existing_entry.entry_id) @@ -214,7 +214,7 @@ async def test_abort_invalid_unique_id(hass): context={"source": SOURCE_USER}, data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -244,7 +244,7 @@ async def test_on_connect_failed(hass, side_effect, error): result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error} @@ -262,7 +262,7 @@ async def test_options_flow(hass): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -276,7 +276,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_CONSIDER_HOME] == 20 assert config_entry.options[CONF_TRACK_UNKNOWN] is True assert config_entry.options[CONF_INTERFACE] == "aaa" diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index a92e73ae18e..9eeddff9a45 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -17,7 +17,7 @@ async def test_show_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocke DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -30,7 +30,7 @@ async def test_adding_second_device( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" with patch( "pyatag.AtagOne.id", @@ -39,7 +39,7 @@ async def test_adding_second_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_connection_error( @@ -53,7 +53,7 @@ async def test_connection_error( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -66,7 +66,7 @@ async def test_unauthorized(hass: HomeAssistant, aioclient_mock: AiohttpClientMo context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unauthorized"} @@ -81,6 +81,6 @@ async def test_full_flow_implementation( context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == UID assert result["result"].unique_id == UID diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index c93e6429f1c..932065f37da 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -1,10 +1,11 @@ """Mocks for the august component.""" from __future__ import annotations +from collections.abc import Iterable import json import os import time -from typing import Any, Iterable +from typing import Any from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from yalexs.activity import ( diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index 9e83810978a..f106b19cea9 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -101,7 +101,7 @@ async def test_option_flow(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -109,6 +109,6 @@ async def test_option_flow(hass): user_input={"forecast_threshold": 65}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"]["forecast_threshold"] == 65 diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index b30d6dc5eeb..73b4b67bc04 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -58,7 +58,7 @@ async def test_form(hass): {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_PORT: "/dev/ttyUSB7", diff --git a/tests/components/aussie_broadband/common.py b/tests/components/aussie_broadband/common.py index abb99355ef3..5d050388b38 100644 --- a/tests/components/aussie_broadband/common.py +++ b/tests/components/aussie_broadband/common.py @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry FAKE_SERVICES = [ { "service_id": "12345678", - "description": "Fake ABB NBN Service", + "description": "Fake ABB NBN Service - AVC123456789", "type": "NBN", "name": "NBN", }, diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py index 8a5f6b6763f..ed98924e19d 100644 --- a/tests/components/aussie_broadband/test_config_flow.py +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.components.aussie_broadband.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .common import FAKE_DATA, FAKE_SERVICES @@ -25,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["errors"] is None with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( @@ -42,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -87,7 +83,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_ABORT + assert result4["type"] == FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 0 @@ -96,7 +92,7 @@ async def test_no_services(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["errors"] is None with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( @@ -111,7 +107,7 @@ async def test_no_services(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_services_found" assert len(mock_setup_entry.mock_calls) == 0 @@ -130,7 +126,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -148,7 +144,7 @@ async def test_form_network_issue(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -176,7 +172,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index 9e2e0b7cccc..3eb1972011c 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -23,7 +23,7 @@ async def test_auth_failure(hass: HomeAssistant) -> None: """Test init with an authentication failure.""" with patch( "homeassistant.components.aussie_broadband.config_flow.ConfigFlow.async_step_reauth", - return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + return_value={"type": data_entry_flow.FlowResultType.FORM}, ) as mock_async_step_reauth: await setup_platform(hass, side_effect=AuthenticationException()) mock_async_step_reauth.assert_called_once() diff --git a/tests/components/aussie_broadband/test_sensor.py b/tests/components/aussie_broadband/test_sensor.py index c99c52d5c86..2db4b79dbe9 100644 --- a/tests/components/aussie_broadband/test_sensor.py +++ b/tests/components/aussie_broadband/test_sensor.py @@ -44,11 +44,17 @@ async def test_nbn_sensor_states(hass): await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_NBN_USAGE) - assert hass.states.get("sensor.nbn_data_used").state == "54321" - assert hass.states.get("sensor.nbn_downloaded").state == "50000" - assert hass.states.get("sensor.nbn_uploaded").state == "4321" - assert hass.states.get("sensor.nbn_billing_cycle_length").state == "28" - assert hass.states.get("sensor.nbn_billing_cycle_remaining").state == "25" + assert hass.states.get("sensor.fake_abb_nbn_service_data_used").state == "54321" + assert hass.states.get("sensor.fake_abb_nbn_service_downloaded").state == "50000" + assert hass.states.get("sensor.fake_abb_nbn_service_uploaded").state == "4321" + assert ( + hass.states.get("sensor.fake_abb_nbn_service_billing_cycle_length").state + == "28" + ) + assert ( + hass.states.get("sensor.fake_abb_nbn_service_billing_cycle_remaining").state + == "25" + ) async def test_phone_sensor_states(hass): @@ -56,12 +62,18 @@ async def test_phone_sensor_states(hass): await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_MOBILE_USAGE) - assert hass.states.get("sensor.mobile_national_calls").state == "1" - assert hass.states.get("sensor.mobile_mobile_calls").state == "2" - assert hass.states.get("sensor.mobile_sms_sent").state == "4" - assert hass.states.get("sensor.mobile_data_used").state == "512" - assert hass.states.get("sensor.mobile_billing_cycle_length").state == "31" - assert hass.states.get("sensor.mobile_billing_cycle_remaining").state == "30" + assert hass.states.get("sensor.fake_abb_mobile_service_national_calls").state == "1" + assert hass.states.get("sensor.fake_abb_mobile_service_mobile_calls").state == "2" + assert hass.states.get("sensor.fake_abb_mobile_service_sms_sent").state == "4" + assert hass.states.get("sensor.fake_abb_mobile_service_data_used").state == "512" + assert ( + hass.states.get("sensor.fake_abb_mobile_service_billing_cycle_length").state + == "31" + ) + assert ( + hass.states.get("sensor.fake_abb_mobile_service_billing_cycle_remaining").state + == "30" + ) async def test_voip_sensor_states(hass): @@ -69,6 +81,10 @@ async def test_voip_sensor_states(hass): await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_VOIP_USAGE) - assert hass.states.get("sensor.mobile_national_calls").state == "1" - assert hass.states.get("sensor.mobile_sms_sent").state == STATE_UNKNOWN - assert hass.states.get("sensor.mobile_data_used").state == STATE_UNKNOWN + assert hass.states.get("sensor.fake_abb_voip_service_national_calls").state == "1" + assert ( + hass.states.get("sensor.fake_abb_voip_service_sms_sent").state == STATE_UNKNOWN + ) + assert ( + hass.states.get("sensor.fake_abb_voip_service_data_used").state == STATE_UNKNOWN + ) diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index edf45742dbd..b0851a3cfe6 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -64,7 +64,7 @@ async def test_ws_setup_depose_mfa(hass, hass_ws_client): assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow["type"] == data_entry_flow.FlowResultType.FORM assert flow["handler"] == "example_module" assert flow["step_id"] == "init" assert flow["data_schema"][0] == {"type": "string", "name": "pin", "required": True} @@ -83,7 +83,7 @@ async def test_ws_setup_depose_mfa(hass, hass_ws_client): assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert flow["handler"] == "example_module" assert flow["data"]["result"] is None diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 47e58fea421..b5bc5f23eaa 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -21,7 +21,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -95,7 +95,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -105,7 +105,7 @@ async def test_reauth(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} @@ -117,7 +117,7 @@ async def test_reauth(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -133,7 +133,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -143,7 +143,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" @@ -160,7 +160,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "foo@bar.com (32406)" assert result["data"][CONF_ACCESS_TOKEN] == CONFIG[CONF_ACCESS_TOKEN] assert result["result"].unique_id == UNIQUE_ID diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 5f30be81d6f..07b2f9ba00f 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -17,7 +17,6 @@ from homeassistant.components.awair.const import ( SENSOR_TYPES_DUST, ) from homeassistant.const import ( - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, @@ -88,7 +87,7 @@ async def test_awair_gen1_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "88", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -116,7 +115,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_CO2].unique_id_tag}", "654.0", { - ATTR_ICON: "mdi:cloud", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, "awair_index": 0.0, }, @@ -129,7 +127,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_VOC].unique_id_tag}", "366", { - ATTR_ICON: "mdi:cloud", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, "awair_index": 1.0, }, @@ -143,7 +140,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_DUST", "14.3", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 1.0, }, @@ -156,7 +152,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM10].unique_id_tag}", "14.3", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 1.0, }, @@ -184,7 +179,7 @@ async def test_awair_gen2_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "97", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -194,7 +189,6 @@ async def test_awair_gen2_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM25].unique_id_tag}", "2.0", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 0.0, }, @@ -218,7 +212,7 @@ async def test_awair_mint_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "98", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -228,7 +222,6 @@ async def test_awair_mint_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM25].unique_id_tag}", "1.0", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 0.0, }, @@ -260,7 +253,7 @@ async def test_awair_glow_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "93", - {ATTR_ICON: "mdi:blur"}, + {}, ) # The glow does not have a particle sensor @@ -280,7 +273,7 @@ async def test_awair_omni_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "99", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -289,7 +282,7 @@ async def test_awair_omni_sensors(hass): "sensor.living_room_sound_level", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SPL_A].unique_id_tag}", "47.0", - {ATTR_ICON: "mdi:ear-hearing", ATTR_UNIT_OF_MEASUREMENT: "dBa"}, + {ATTR_UNIT_OF_MEASUREMENT: "dBa"}, ) assert_expected_properties( @@ -333,7 +326,7 @@ async def test_awair_unavailable(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "88", - {ATTR_ICON: "mdi:blur"}, + {}, ) with patch("python_awair.AwairClient.query", side_effect=OFFLINE_FIXTURE): @@ -344,5 +337,5 @@ async def test_awair_unavailable(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", STATE_UNAVAILABLE, - {ATTR_ICON: "mdi:blur"}, + {}, ) diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index f09e87020c3..2daf350ac93 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest import respx -from homeassistant import data_entry_flow from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( @@ -31,11 +30,7 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .test_device import ( DEFAULT_HOST, @@ -57,7 +52,7 @@ async def test_flow_manual_configuration(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with respx.mock: @@ -72,7 +67,7 @@ async def test_flow_manual_configuration(hass): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -93,7 +88,7 @@ async def test_manual_configuration_update_configuration(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -112,7 +107,7 @@ async def test_manual_configuration_update_configuration(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert device.host == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 @@ -124,11 +119,11 @@ async def test_flow_fails_faulty_credentials(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.AuthenticationRequired, ): result = await hass.config_entries.flow.async_configure( @@ -150,11 +145,11 @@ async def test_flow_fails_cannot_connect(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.CannotConnect, ): result = await hass.config_entries.flow.async_configure( @@ -187,7 +182,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with respx.mock: @@ -202,7 +197,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -227,7 +222,7 @@ async def test_reauth_flow_update_configuration(hass): data=config_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with respx.mock: @@ -243,7 +238,7 @@ async def test_reauth_flow_update_configuration(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert device.host == "2.3.4.5" assert device.username == "user2" @@ -317,7 +312,7 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER flows = hass.config_entries.flow.async_progress() @@ -336,7 +331,7 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -398,7 +393,7 @@ async def test_discovered_device_already_configured( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == DEFAULT_HOST @@ -467,7 +462,7 @@ async def test_discovery_flow_updated_configuration( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data == { CONF_HOST: "2.3.4.5", @@ -525,7 +520,7 @@ async def test_discovery_flow_ignore_non_axis_device( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_axis_device" @@ -574,7 +569,7 @@ async def test_discovery_flow_ignore_link_local_address( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "link_local_address" @@ -591,7 +586,7 @@ async def test_option_flow(hass): device.config_entry.entry_id ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "configure_stream" assert set(result["data_schema"].schema[CONF_STREAM_PROFILE].container) == { DEFAULT_STREAM_PROFILE, @@ -608,7 +603,7 @@ async def test_option_flow(hass): user_input={CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EVENTS: True, CONF_STREAM_PROFILE: "profile_1", diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 4717e2915c1..ba6df6e2e2d 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -441,7 +441,7 @@ async def test_device_reset(hass): async def test_device_not_accessible(hass): """Failed setup schedules a retry of setup.""" - with patch.object(axis.device, "get_device", side_effect=axis.errors.CannotConnect): + with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -449,7 +449,7 @@ async def test_device_not_accessible(hass): async def test_device_trigger_reauth_flow(hass): """Failed authentication trigger a reauthentication flow.""" with patch.object( - axis.device, "get_device", side_effect=axis.errors.AuthenticationRequired + axis, "get_axis_device", side_effect=axis.errors.AuthenticationRequired ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await setup_axis_integration(hass) mock_flow_init.assert_called_once() @@ -458,7 +458,7 @@ async def test_device_trigger_reauth_flow(hass): async def test_device_unknown_error(hass): """Unknown errors are handled.""" - with patch.object(axis.device, "get_device", side_effect=Exception): + with patch.object(axis, "get_axis_device", side_effect=Exception): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -468,7 +468,7 @@ async def test_new_event_sends_signal(hass): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send: axis_device.async_event_callback(action=OPERATION_INITIALIZED, event_id="event") @@ -484,8 +484,7 @@ async def test_shutdown(): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) - axis_device.api = Mock() + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) await axis_device.shutdown(None) @@ -497,7 +496,7 @@ async def test_get_device_fails(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_device_unavailable(hass): @@ -505,7 +504,7 @@ async def test_get_device_device_unavailable(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_unknown_error(hass): @@ -513,4 +512,4 @@ async def test_get_device_unknown_error(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index 7817f6fc570..713d3f31a2f 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -27,7 +27,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -41,7 +41,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -50,7 +50,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -67,7 +67,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +76,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "invalid_auth"} @@ -91,7 +91,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -100,7 +100,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -117,7 +117,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -126,7 +126,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "cannot_connect"} @@ -146,7 +146,7 @@ async def test_project_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -155,7 +155,7 @@ async def test_project_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "project_error"} @@ -177,7 +177,7 @@ async def test_reauth_project_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -186,7 +186,7 @@ async def test_reauth_project_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "project_error"} @@ -208,7 +208,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" assert result["errors"] == {"base": "invalid_auth"} @@ -229,7 +229,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -253,7 +253,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -263,7 +263,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result2["title"] == f"{FIXTURE_USER_INPUT[CONF_ORG]}/{FIXTURE_USER_INPUT[CONF_PROJECT]}" diff --git a/tests/components/azure_event_hub/test_config_flow.py b/tests/components/azure_event_hub/test_config_flow.py index 4e135d55555..93615561289 100644 --- a/tests/components/azure_event_hub/test_config_flow.py +++ b/tests/components/azure_event_hub/test_config_flow.py @@ -63,7 +63,7 @@ async def test_form( result2["flow_id"], step2_config.copy(), ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "test-instance" assert result3["data"] == data_config mock_setup_entry.assert_called_once() @@ -79,7 +79,7 @@ async def test_import(hass, mock_setup_entry): data=IMPORT_CONFIG.copy(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test-instance" options = { CONF_SEND_INTERVAL: import_config.pop(CONF_SEND_INTERVAL), @@ -109,7 +109,7 @@ async def test_single_instance(hass, source): context={"source": source}, data=BASE_CONFIG_CS.copy(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -138,7 +138,7 @@ async def test_connection_error_sas( result["flow_id"], SAS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -168,7 +168,7 @@ async def test_connection_error_cs( result["flow_id"], CS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -176,13 +176,13 @@ async def test_options_flow(hass, entry): """Test options flow.""" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], UPDATE_OPTIONS ) - assert updated["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert updated["data"] == UPDATE_OPTIONS await hass.async_block_till_done() diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index 77a83a9673b..0df9ce480a7 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.baf.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_NAME, MOCK_UUID, MockBAFDevice @@ -45,7 +41,7 @@ async def test_form_user(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +59,7 @@ async def test_form_cannot_connect(hass): {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -79,7 +75,7 @@ async def test_form_unknown_exception(hass): {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -99,7 +95,7 @@ async def test_zeroconf_discovery(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -137,7 +133,7 @@ async def test_zeroconf_updates_existing_ip(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "127.0.0.1" @@ -157,7 +153,7 @@ async def test_zeroconf_rejects_ipv6(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" @@ -176,7 +172,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass): type="mock_type", ), ) - assert discovery_result["type"] == RESULT_TYPE_FORM + assert discovery_result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -194,7 +190,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py index ffb8c80a29e..d6be9feb727 100644 --- a/tests/components/balboa/test_config_flow.py +++ b/tests/components/balboa/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +21,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -41,7 +37,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +58,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client: MagicMock) -> No TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -82,7 +78,7 @@ async def test_unknown_error(hass: HomeAssistant, client: MagicMock) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -94,7 +90,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -110,7 +106,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -124,7 +120,7 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -137,5 +133,5 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert dict(config_entry.options) == {CONF_SYNC_TIME: True} diff --git a/tests/components/blebox/test_button.py b/tests/components/blebox/test_button.py new file mode 100644 index 00000000000..8d89f31d260 --- /dev/null +++ b/tests/components/blebox/test_button.py @@ -0,0 +1,65 @@ +"""Blebox button entities tests.""" +import logging +from unittest.mock import PropertyMock + +import blebox_uniapi +import pytest + +from homeassistant.const import ATTR_ICON + +from .conftest import async_setup_entity, mock_feature + +query_icon_matching = [ + ("up", "mdi:arrow-up-circle"), + ("down", "mdi:arrow-down-circle"), + ("fav", "mdi:heart-circle"), + ("open", "mdi:arrow-up-circle"), + ("close", "mdi:arrow-down-circle"), +] + + +@pytest.fixture(name="tvliftbox") +def tv_lift_box_fixture(caplog): + """Return simple button entity mock.""" + caplog.set_level(logging.ERROR) + + feature = mock_feature( + "buttons", + blebox_uniapi.button.Button, + unique_id="BleBox-tvLiftBox-4a3fdaad90aa-open_or_stop", + full_name="tvLiftBox-open_or_stop", + control_type=blebox_uniapi.button.ControlType.OPEN, + ) + + product = feature.product + type(product).name = PropertyMock(return_value="My tvLiftBox") + type(product).model = PropertyMock(return_value="tvLiftBox") + type(product)._query_string = PropertyMock(return_value="open_or_stop") + + return (feature, "button.tvliftbox_open_or_stop") + + +async def test_tvliftbox_init(tvliftbox, hass, config, caplog): + """Test tvLiftBox initialisation.""" + caplog.set_level(logging.ERROR) + + _, entity_id = tvliftbox + entry = await async_setup_entity(hass, config, entity_id) + state = hass.states.get(entity_id) + + assert entry.unique_id == "BleBox-tvLiftBox-4a3fdaad90aa-open_or_stop" + + assert state.name == "tvLiftBox-open_or_stop" + + +@pytest.mark.parametrize("input", query_icon_matching) +async def test_get_icon(input, tvliftbox, hass, config, caplog): + """Test if proper icon is returned.""" + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = tvliftbox + feature_mock.query_string = input[0] + _ = await async_setup_entity(hass, config, entity_id) + state = hass.states.get(entity_id) + + assert state.attributes[ATTR_ICON] == input[1] diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 3f40880abf7..7db216e294e 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -159,7 +159,7 @@ async def test_already_configured(hass, valid_feature_mock): context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "address_already_configured" diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 5ea03eb2b62..999204a2d91 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -250,7 +250,7 @@ async def test_reauth_shows_user_step(hass): context={"source": config_entries.SOURCE_REAUTH}, data={"username": "blink@example.com", "password": "invalid_password"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -280,7 +280,7 @@ async def test_options_flow(hass): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "simple_options" result = await hass.config_entries.options.async_configure( @@ -288,6 +288,6 @@ async def test_options_flow(hass): user_input={"scan_interval": 5}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {"scan_interval": 5} assert mock_blink.refresh_rate == 5 diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py new file mode 100644 index 00000000000..3dc80d55590 --- /dev/null +++ b/tests/components/bluetooth/__init__.py @@ -0,0 +1,8 @@ +"""Tests for the Bluetooth integration.""" + +from homeassistant.components.bluetooth import models + + +def _get_underlying_scanner(): + """Return the underlying scanner that has been wrapped.""" + return models.HA_BLEAK_SCANNER diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py new file mode 100644 index 00000000000..760500fe7a1 --- /dev/null +++ b/tests/components/bluetooth/conftest.py @@ -0,0 +1 @@ +"""Tests for the bluetooth component.""" diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py new file mode 100644 index 00000000000..1053133cac9 --- /dev/null +++ b/tests/components/bluetooth/test_config_flow.py @@ -0,0 +1,240 @@ +"""Test the bluetooth config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.bluetooth.const import ( + CONF_ADAPTER, + DOMAIN, + MACOS_DEFAULT_BLUETOOTH_ADAPTER, +) +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_async_step_user(hass): + """Test setting up manually.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "enable_bluetooth" + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Bluetooth" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_user_only_allows_one(hass): + """Test setting up manually with an existing entry.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_integration_discovery(hass): + """Test setting up from integration discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "enable_bluetooth" + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Bluetooth" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_integration_discovery_during_onboarding(hass): + """Test setting up from integration discovery during onboarding.""" + + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Bluetooth" + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + +async def test_async_step_integration_discovery_already_exists(hass): + """Test setting up from integration discovery when an entry already exists.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_import(hass): + """Test setting up from integration discovery.""" + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Bluetooth" + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_import_already_exists(hass): + """Test setting up from yaml when an entry already exists.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Linux") +async def test_options_flow_linux(mock_system, hass, mock_bleak_scanner_start): + """Test options on Linux.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="DOMAIN", + ) + entry.add_to_hass(hass) + + # Verify we can keep it as hci0 + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADAPTER: "hci0", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_ADAPTER] == "hci0" + + # Verify we can change it to hci1 + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] + ): + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADAPTER: "hci1", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_ADAPTER] == "hci1" + + +@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Darwin") +async def test_options_flow_macos(mock_system, hass, mock_bleak_scanner_start): + """Test options on MacOS.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="DOMAIN", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADAPTER: MACOS_DEFAULT_BLUETOOTH_ADAPTER, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_ADAPTER] == MACOS_DEFAULT_BLUETOOTH_ADAPTER + + +@patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Windows" +) +async def test_options_flow_windows(mock_system, hass, mock_bleak_scanner_start): + """Test options on Windows.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="DOMAIN", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_adapters" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py new file mode 100644 index 00000000000..edc5eb024a6 --- /dev/null +++ b/tests/components/bluetooth/test_init.py @@ -0,0 +1,1521 @@ +"""Tests for the Bluetooth integration.""" +import asyncio +from datetime import timedelta +from unittest.mock import MagicMock, patch + +from bleak import BleakError +from bleak.backends.scanner import AdvertisementData, BLEDevice +from dbus_next import InvalidMessageError +import pytest + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import ( + SOURCE_LOCAL, + UNAVAILABLE_TRACK_SECONDS, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfo, + async_process_advertisements, + async_track_unavailable, + models, +) +from homeassistant.components.bluetooth.const import ( + CONF_ADAPTER, + UNIX_DEFAULT_BLUETOOTH_ADAPTER, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant, callback +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import _get_underlying_scanner + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_setup_and_stop(hass, mock_bleak_scanner_start, enable_bluetooth): + """Test we and setup and stop the scanner.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + +async def test_setup_and_stop_no_bluetooth(hass, caplog): + """Test we fail gracefully when bluetooth is not available.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + side_effect=BleakError, + ) as mock_ha_bleak_scanner, patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_ha_bleak_scanner.mock_calls) == 1 + assert "Failed to initialize Bluetooth" in caplog.text + + +async def test_setup_and_stop_broken_bluetooth(hass, caplog): + """Test we fail gracefully when bluetooth/dbus is broken.""" + mock_bt = [] + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BleakError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "Failed to start Bluetooth" in caplog.text + assert len(bluetooth.async_discovered_service_info(hass)) == 0 + + +async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): + """Test we fail gracefully when bluetooth/dbus is hanging.""" + mock_bt = [] + + async def _mock_hang(): + await asyncio.sleep(1) + + with patch.object(bluetooth, "START_TIMEOUT", 0), patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=_mock_hang, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "Timed out starting Bluetooth" in caplog.text + + +async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): + """Test we retry if the adapter is not yet available.""" + mock_bt = [] + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BleakError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + + assert "Failed to start Bluetooth" in caplog.text + assert len(bluetooth.async_discovered_service_info(hass)) == 0 + assert entry.state == ConfigEntryState.SETUP_RETRY + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop", + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): + """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" + mock_bt = [] + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + side_effect=FileNotFoundError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "Failed to initialize Bluetooth" in caplog.text + assert not bluetooth.async_discovered_service_info(hass) + assert not bluetooth.async_address_present(hass, "aa:bb:bb:dd:ee:ff") + + +async def test_discovery_match_by_service_uuid( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test bluetooth discovery match by service_uuid.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + + _get_underlying_scanner()._callback(wrong_device, wrong_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "switchbot" + + +async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): + """Test bluetooth discovery match by local_name.""" + mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + + _get_underlying_scanner()._callback(wrong_device, wrong_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "switchbot" + + +async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( + hass, mock_bleak_scanner_start +): + """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start.""" + mock_bt = [ + { + "domain": "homekit_controller", + "manufacturer_id": 76, + "manufacturer_data_start": [0x06, 0x02, 0x03], + } + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + hkc_device = BLEDevice("44:44:33:11:23:45", "lock") + hkc_adv_no_mfr_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={}, + ) + hkc_adv = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={76: b"\x06\x02\x03\x99"}, + ) + + # 1st discovery with no manufacturer data + # should not trigger config flow + _get_underlying_scanner()._callback(hkc_device, hkc_adv_no_mfr_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 2nd discovery with manufacturer data + # should trigger a config flow + _get_underlying_scanner()._callback(hkc_device, hkc_adv) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller" + mock_config_flow.reset_mock() + + # 3rd discovery should not generate another flow + _get_underlying_scanner()._callback(hkc_device, hkc_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + mock_config_flow.reset_mock() + not_hkc_device = BLEDevice("44:44:33:11:23:21", "lock") + not_hkc_adv = AdvertisementData( + local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} + ) + + _get_underlying_scanner()._callback(not_hkc_device, not_hkc_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + not_apple_device = BLEDevice("44:44:33:11:23:23", "lock") + not_apple_adv = AdvertisementData( + local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} + ) + + _get_underlying_scanner()._callback(not_apple_device, not_apple_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + +async def test_discovery_match_by_service_data_uuid_then_others( + hass, mock_bleak_scanner_start +): + """Test bluetooth discovery match by service_data_uuid and then other fields.""" + mock_bt = [ + { + "domain": "my_domain", + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb", + }, + { + "domain": "my_domain", + "service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc", + }, + { + "domain": "other_domain", + "manufacturer_id": 323, + }, + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + device = BLEDevice("44:44:33:11:23:45", "lock") + adv_without_service_data_uuid = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={}, + ) + adv_with_mfr_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={323: b"\x01\x02\x03"}, + service_data={}, + ) + adv_with_service_data_uuid = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"}, + ) + adv_with_service_data_uuid_and_mfr_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={323: b"\x01\x02\x03"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"}, + ) + adv_with_service_data_uuid_and_mfr_data_and_service_uuid = AdvertisementData( + local_name="lock", + manufacturer_data={323: b"\x01\x02\x03"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"}, + service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"], + ) + adv_with_service_uuid = AdvertisementData( + local_name="lock", + manufacturer_data={}, + service_data={}, + service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"], + ) + # 1st discovery should not generate a flow because the + # service_data_uuid is not in the advertisement + _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 2nd discovery should not generate a flow because the + # service_data_uuid is not in the advertisement + _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 3rd discovery should generate a flow because the + # manufacturer_data is in the advertisement + _get_underlying_scanner()._callback(device, adv_with_mfr_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "other_domain" + mock_config_flow.reset_mock() + + # 4th discovery should generate a flow because the + # service_data_uuid is in the advertisement and + # we never saw a service_data_uuid before + _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "my_domain" + mock_config_flow.reset_mock() + + # 5th discovery should not generate a flow because the + # we already saw an advertisement with the service_data_uuid + _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 6th discovery should not generate a flow because the + # manufacturer_data is in the advertisement + # and we saw manufacturer_data before + _get_underlying_scanner()._callback( + device, adv_with_service_data_uuid_and_mfr_data + ) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 7th discovery should generate a flow because the + # service_uuids is in the advertisement + # and we never saw service_uuids before + _get_underlying_scanner()._callback( + device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid + ) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 2 + assert { + mock_config_flow.mock_calls[0][1][0], + mock_config_flow.mock_calls[1][1][0], + } == {"my_domain", "other_domain"} + mock_config_flow.reset_mock() + + # 8th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback( + device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid + ) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 9th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback(device, adv_with_service_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 10th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 11th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + +async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( + hass, mock_bleak_scanner_start +): + """Test bluetooth discovery matches twice for service_uuid and then manufacturer_id.""" + mock_bt = [ + { + "domain": "my_domain", + "manufacturer_id": 76, + }, + { + "domain": "my_domain", + "service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc", + }, + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + device = BLEDevice("44:44:33:11:23:45", "lock") + adv_service_uuids = AdvertisementData( + local_name="lock", + service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fc"], + manufacturer_data={}, + ) + adv_manufacturer_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={76: b"\x06\x02\x03\x99"}, + ) + + # 1st discovery with matches service_uuid + # should trigger config flow + _get_underlying_scanner()._callback(device, adv_service_uuids) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "my_domain" + mock_config_flow.reset_mock() + + # 2nd discovery with manufacturer data + # should trigger a config flow + _get_underlying_scanner()._callback(device, adv_manufacturer_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "my_domain" + mock_config_flow.reset_mock() + + # 3rd discovery should not generate another flow + _get_underlying_scanner()._callback(device, adv_service_uuids) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 4th discovery should not generate another flow + _get_underlying_scanner()._callback(device, adv_manufacturer_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + +async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): + """Test the async_discovered_device API.""" + mock_bt = [] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + assert not bluetooth.async_discovered_service_info(hass) + assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + assert not bluetooth.async_discovered_service_info(hass) + + wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False + + @callback + def _wrong_device_unavailable_callback(_address: str) -> None: + """Wrong device unavailable callback.""" + nonlocal wrong_device_went_unavailable + wrong_device_went_unavailable = True + raise ValueError("blow up") + + @callback + def _switchbot_device_unavailable_callback(_address: str) -> None: + """Switchbot device unavailable callback.""" + nonlocal switchbot_device_went_unavailable + switchbot_device_went_unavailable = True + + wrong_device_unavailable_cancel = async_track_unavailable( + hass, _wrong_device_unavailable_callback, wrong_device.address + ) + switchbot_device_unavailable_cancel = async_track_unavailable( + hass, _switchbot_device_unavailable_callback, switchbot_device.address + ) + + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + + service_infos = bluetooth.async_discovered_service_info(hass) + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is True + + # See the devices again + _get_underlying_scanner()._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + # Cancel the callbacks + wrong_device_unavailable_cancel() + switchbot_device_unavailable_cancel() + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False + + # Verify the cancel is effective + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is False + + assert len(service_infos) == 1 + # wrong_name should not appear because bleak no longer sees it + assert service_infos[0].name == "wohand" + assert service_infos[0].source == SOURCE_LOCAL + assert isinstance(service_infos[0].device, BLEDevice) + assert isinstance(service_infos[0].advertisement, AdvertisementData) + + assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False + assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True + + +async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetooth): + """Test registering a callback.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + if len(callbacks) >= 3: + raise ValueError + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"service_uuids": {"cba20d00-224d-11e6-9fb8-0002a5d5c51b"}}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + _get_underlying_scanner()._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + # 3rd callback raises ValueError but is still tracked + _get_underlying_scanner()._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + # 4th callback should not be tracked since we canceled + _get_underlying_scanner()._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + assert len(callbacks) == 3 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "wohand" + assert service_info.source == SOURCE_LOCAL + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + service_info: BluetoothServiceInfo = callbacks[1][0] + assert service_info.name == "empty" + assert service_info.source == SOURCE_LOCAL + assert service_info.manufacturer is None + assert service_info.manufacturer_id is None + + service_info: BluetoothServiceInfo = callbacks[2][0] + assert service_info.name == "empty" + assert service_info.source == SOURCE_LOCAL + assert service_info.manufacturer is None + assert service_info.manufacturer_id is None + + +async def test_register_callback_by_address( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by address.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + if len(callbacks) >= 3: + raise ValueError + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + _get_underlying_scanner()._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + # 3rd callback raises ValueError but is still tracked + _get_underlying_scanner()._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + # 4th callback should not be tracked since we canceled + _get_underlying_scanner()._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + # Now register again with a callback that fails to + # make sure we do not perm fail + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + ) + cancel() + + # Now register again, since the 3rd callback + # should fail but we should still record it + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + ) + cancel() + + assert len(callbacks) == 3 + + for idx in range(3): + service_info: BluetoothServiceInfo = callbacks[idx][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + +async def test_process_advertisements_bail_on_good_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Test as soon as we see a 'good' advertisement we return it.""" + done = asyncio.Future() + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set_result(None) + return len(service_info.service_data) > 0 + + handle = hass.async_create_task( + async_process_advertisements( + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, + ) + ) + + while not done.done(): + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.name == "wohand" + + +async def test_process_advertisements_ignore_bad_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Check that we ignore bad advertisements.""" + done = asyncio.Event() + return_value = asyncio.Event() + + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""}, + ) + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set() + return return_value.is_set() + + handle = hass.async_create_task( + async_process_advertisements( + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, + ) + ) + + # The goal of this loop is to make sure that async_process_advertisements sees at least one + # callback that returns False + while not done.is_set(): + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + # Set the return value and mutate the advertisement + # Check that scan ends and correct advertisement data is returned + return_value.set() + adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c" + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.service_data["00000d00-0000-1000-8000-00805f9b34fa"] == b"H\x10c" + + +async def test_process_advertisements_timeout( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test we timeout if no advertisements at all.""" + + def _callback(service_info: BluetoothServiceInfo) -> bool: + return False + + with pytest.raises(asyncio.TimeoutError): + await async_process_advertisements( + hass, _callback, {}, BluetoothScanningMode.ACTIVE, 0 + ) + + +async def test_wrapped_instance_with_filter( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert _get_underlying_scanner() is not None + scanner = models.HaBleakScannerWrapper( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) + + mock_discovered = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = mock_discovered + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 1 + assert discovered == mock_discovered + assert len(detected) == 1 + + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 2 + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 3 + + type(_get_underlying_scanner()).discovered_devices = [] + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 0 + assert discovered == [] + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + assert len(detected) == 4 + + # The filter we created in the wrapped scanner with should be respected + # and we should not get another callback + _get_underlying_scanner()._callback(empty_device, empty_adv) + assert len(detected) == 4 + + +async def test_wrapped_instance_with_service_uuids( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert _get_underlying_scanner() is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) + + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] + for _ in range(2): + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + _get_underlying_scanner()._callback(empty_device, empty_adv) + assert len(detected) == 2 + + +async def test_wrapped_instance_with_broken_callbacks( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test broken callbacks do not cause the scanner to fail.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + if detected: + raise ValueError + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + assert _get_underlying_scanner() is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + assert len(detected) == 1 + + +async def test_wrapped_instance_changes_uuids( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test consumers can use the wrapped instance can change the uuids later.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert _get_underlying_scanner() is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) + + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] + for _ in range(2): + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + _get_underlying_scanner()._callback(empty_device, empty_adv) + assert len(detected) == 2 + + +async def test_wrapped_instance_changes_filters( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test consumers can use the wrapped instance can change the filter later.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:42", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:62", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert _get_underlying_scanner() is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) + + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] + for _ in range(2): + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + _get_underlying_scanner()._callback(empty_device, empty_adv) + assert len(detected) == 2 + + +async def test_wrapped_instance_unsupported_filter( + hass, mock_bleak_scanner_start, caplog, enable_bluetooth +): + """Test we want when their filter is ineffective.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert _get_underlying_scanner() is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={ + "unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + "DuplicateData": True, + } + ) + assert "Only UUIDs filters are supported" in caplog.text + + +async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): + """Test the async_ble_device_from_address api.""" + mock_bt = [] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + assert not bluetooth.async_discovered_service_info(hass) + assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") + assert ( + bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") is None + ) + + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + assert not bluetooth.async_discovered_service_info(hass) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert ( + bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") + is switchbot_device + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, "00:66:33:22:11:22") is None + ) + + +async def test_setup_without_bluetooth_in_configuration_yaml(hass, mock_bluetooth): + """Test setting up without bluetooth in configuration.yaml does not create the config entry.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + + +async def test_setup_with_bluetooth_in_configuration_yaml(hass, mock_bluetooth): + """Test setting up with bluetooth in configuration.yaml creates the config entry.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) + await hass.async_block_till_done() + assert hass.config_entries.async_entries(bluetooth.DOMAIN) + + +async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_bluetooth): + """Test we can setup and unsetup bluetooth.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry.add_to_hass(hass) + for _ in range(2): + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_auto_detect_bluetooth_adapters_linux(hass): + """Test we auto detect bluetooth adapters on linux.""" + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0"] + ), patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + + +async def test_auto_detect_bluetooth_adapters_linux_multiple(hass): + """Test we auto detect bluetooth adapters on linux with multiple adapters.""" + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci1", "hci0"] + ), patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + + +async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): + """Test we auto detect bluetooth adapters on linux with no adapters found.""" + with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()), patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 + + +async def test_auto_detect_bluetooth_adapters_macos(hass): + """Test we auto detect bluetooth adapters on macos.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Darwin" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + + +async def test_no_auto_detect_bluetooth_adapters_windows(hass): + """Test we auto detect bluetooth adapters on windows.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", + return_value="Windows", + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 + + +async def test_raising_runtime_error_when_no_bluetooth(hass): + """Test we raise an exception if we try to get the scanner when its not there.""" + with pytest.raises(RuntimeError): + bluetooth.async_get_scanner(hass) + + +async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_bluetooth): + """Test getting the scanner returns the wrapped instance.""" + scanner = bluetooth.async_get_scanner(hass) + assert isinstance(scanner, models.HaBleakScannerWrapper) + + +async def test_config_entry_can_be_reloaded_when_stop_raises( + hass, caplog, enable_bluetooth +): + """Test we can reload if stopping the scanner raises.""" + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop", side_effect=BleakError + ): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert "Error stopping scanner" in caplog.text + + +async def test_changing_the_adapter_at_runtime(hass): + """Test we can change the adapter at runtime.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, + data={}, + options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + ) as mock_setup, patch( + "homeassistant.components.bluetooth.HaBleakScanner.start" + ), patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop" + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "adapter" not in mock_setup.mock_calls[0][2] + + entry.options = {CONF_ADAPTER: "hci1"} + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + assert mock_setup.mock_calls[1][2]["adapter"] == "hci1" + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_dbus_socket_missing_in_container(hass, caplog): + """Test we handle dbus being missing in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "/run/dbus" in caplog.text + assert "docker" in caplog.text + + +async def test_dbus_socket_missing(hass, caplog): + """Test we handle dbus being missing.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "docker" not in caplog.text + + +async def test_dbus_broken_pipe_in_container(hass, caplog): + """Test we handle dbus broken pipe in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text + assert "restarting" in caplog.text + assert "container" in caplog.text + + +async def test_dbus_broken_pipe(hass, caplog): + """Test we handle dbus broken pipe.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "restarting" in caplog.text + assert "container" not in caplog.text + + +async def test_invalid_dbus_message(hass, caplog): + """Test we handle invalid dbus message.""" + + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=InvalidMessageError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py new file mode 100644 index 00000000000..31530cd6995 --- /dev/null +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -0,0 +1,242 @@ +"""Tests for the Bluetooth integration PassiveBluetoothDataUpdateCoordinator.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any +from unittest.mock import MagicMock, patch + +from homeassistant.components.bluetooth import ( + DOMAIN, + UNAVAILABLE_TRACK_SECONDS, + BluetoothChange, + BluetoothScanningMode, +) +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import _get_underlying_scanner + +from tests.common import async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) + + +class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): + """An example coordinator that subclasses PassiveBluetoothDataUpdateCoordinator.""" + + def __init__(self, hass, logger, device_id, mode) -> None: + """Initialize the coordinator.""" + super().__init__(hass, logger, device_id, mode) + self.data: dict[str, Any] = {} + + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.data = {"rssi": service_info.rssi} + super()._async_handle_bluetooth_event(service_info, change) + + +async def test_basic_usage(hass, mock_bleak_scanner_start): + """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + mock_listener = MagicMock() + unregister_listener = coordinator.async_add_listener(mock_listener) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + assert len(mock_listener.mock_calls) == 1 + assert coordinator.data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} + assert coordinator.available is True + + unregister_listener() + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + assert len(mock_listener.mock_calls) == 1 + assert coordinator.data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} + assert coordinator.available is True + cancel() + + +async def test_context_compatiblity_with_data_update_coordinator( + hass, mock_bleak_scanner_start +): + """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + mock_listener = MagicMock() + coordinator.async_add_listener(mock_listener) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_start() + + assert not set(coordinator.async_contexts()) + + def update_callback1(): + pass + + def update_callback2(): + pass + + unsub1 = coordinator.async_add_listener(update_callback1, 1) + assert set(coordinator.async_contexts()) == {1} + + unsub2 = coordinator.async_add_listener(update_callback2, 2) + assert set(coordinator.async_contexts()) == {1, 2} + + unsub1() + assert set(coordinator.async_contexts()) == {2} + + unsub2() + assert not set(coordinator.async_contexts()) + + +async def test_unavailable_callbacks_mark_the_coordinator_unavailable( + hass, mock_bleak_scanner_start +): + """Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device.""" + with patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.PASSIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + mock_listener = MagicMock() + coordinator.async_add_listener(mock_listener) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_start() + + assert coordinator.available is False + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + scanner = _get_underlying_scanner() + + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch.object( + scanner, + "history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert coordinator.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch.object( + scanner, + "history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert coordinator.available is False + + +async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start): + """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + entity = PassiveBluetoothCoordinatorEntity(coordinator) + assert entity.available is False + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_start() + + assert coordinator.available is False + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + entity.hass = hass + await entity.async_update() + assert entity.available is True diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py new file mode 100644 index 00000000000..6a092746a68 --- /dev/null +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -0,0 +1,1077 @@ +"""Tests for the Bluetooth integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from unittest.mock import MagicMock, patch + +from home_assistant_bluetooth import BluetoothServiceInfo +import pytest + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth import ( + DOMAIN, + UNAVAILABLE_TRACK_SECONDS, + BluetoothChange, + BluetoothScanningMode, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import CoreState, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import _get_underlying_scanner + +from tests.common import MockEntityPlatform, async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_names={ + PassiveBluetoothEntityKey("temperature", None): "Temperature", + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_basic_usage(hass, mock_bleak_scanner_start): + """Test basic usage of the PassiveBluetoothProcessorCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + entity_key = PassiveBluetoothEntityKey("temperature", None) + entity_key_events = [] + all_events = [] + mock_entity = MagicMock() + mock_add_entities = MagicMock() + + def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock entity key listener.""" + entity_key_events.append(data) + + cancel_async_add_entity_key_listener = processor.async_add_entity_key_listener( + _async_entity_key_listener, + entity_key, + ) + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + cancel_listener = processor.async_add_listener( + _all_listener, + ) + + cancel_async_add_entities_listener = processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 1 + assert len(all_events) == 1 + + # There should be 4 calls to create entities + assert len(mock_entity.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + + # On the second, the entities should already be created + # so the mock should not be called again + assert len(mock_entity.mock_calls) == 2 + + cancel_async_add_entity_key_listener() + cancel_listener() + cancel_async_add_entities_listener() + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should not trigger any more now + # that they were cancelled + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + assert len(mock_entity.mock_calls) == 2 + assert coordinator.available is True + + unregister_processor() + cancel_coordinator() + + +async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): + """Test that the coordinator is unavailable after no data for a while.""" + with patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + mock_entity = MagicMock() + mock_add_entities = MagicMock() + processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + assert coordinator.available is False + assert processor.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 + assert coordinator.available is True + assert processor.available is True + scanner = _get_underlying_scanner() + + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch.object( + scanner, + "history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert coordinator.available is False + assert processor.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 + assert coordinator.available is True + assert processor.available is True + + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch.object( + scanner, + "history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert coordinator.available is False + assert processor.available is False + + unregister_processor() + cancel_coordinator() + + +async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): + """Test updates are ignored once hass is stopping.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + all_events = [] + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + processor.async_add_listener( + _all_listener, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + + hass.state = CoreState.stopping + + # We should stop processing events once hass is stopping + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + unregister_processor() + cancel_coordinator() + + +async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): + """Test we handle exceptions from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + raise Exception("Test exception") + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + processor.async_add_listener(MagicMock()) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + + # We should go unavailable once we get an exception + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert "Test exception" in caplog.text + assert processor.available is False + + # We should go available again once we get data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + unregister_processor() + cancel_coordinator() + + +async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): + """Test we handle bad data from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + return "bad_data" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + processor.async_add_listener(MagicMock()) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + + # We should go unavailable once we get bad data + with pytest.raises(ValueError): + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + assert processor.available is False + + # We should go available again once we get good data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + unregister_processor() + cancel_coordinator() + + +GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x04\xb5\xa2d\x00\x06L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) +GOVEE_B5178_PRIMARY_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-92, + manufacturer_data={ + 1: b"\x01\x01\x00\x03\x07Xd\x00\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) + +GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_names={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -95, + }, +) +GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( + PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + "primary": { + "name": "B5178D6FB Primary", + "manufacturer": "Govee", + "model": "H5178", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): SensorEntityDescription( + key="temperature_primary", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="primary" + ): SensorEntityDescription( + key="humidity_primary", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="primary" + ): SensorEntityDescription( + key="battery_primary", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): SensorEntityDescription( + key="signal_strength_primary", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_names={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="primary"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): "Signal Strength", + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -92, + PassiveBluetoothEntityKey(key="temperature", device_id="primary"): 19.8488, + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): 48.8, + PassiveBluetoothEntityKey(key="battery", device_id="primary"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="primary"): -92, + }, + ) +) + + +async def test_integration_with_entity(hass, mock_bleak_scanner_start): + """Test integration of PassiveBluetoothProcessorCoordinator with PassiveBluetoothCoordinatorEntity.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + update_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal update_count + update_count += 1 + if update_count > 2: + return GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + processor.async_add_listener(MagicMock()) + + mock_add_entities = MagicMock() + + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Third call with primary and remote sensor entities adds the primary sensor entities + assert len(mock_add_entities.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Forth call with both primary and remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 2 + + entities = [ + *mock_add_entities.mock_calls[0][1][0], + *mock_add_entities.mock_calls[1][1][0], + ] + + entity_one: PassiveBluetoothProcessorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff-remote")}, + "manufacturer": "Govee", + "model": "H5178-REMOTE", + "name": "B5178D6FB Remote", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ) + cancel_coordinator() + + +NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={}, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + name="Pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start): + """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + mock_add_entities = MagicMock() + + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_entities, + ) + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + entities = mock_add_entities.mock_calls[0][1][0] + entity_one: PassiveBluetoothProcessorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "name": "Generic", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id=None + ) + cancel_coordinator() + + +async def test_passive_bluetooth_entity_with_entity_platform( + hass, mock_bleak_scanner_start +): + """Test with a mock entity platform.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + entity_platform = MockEntityPlatform(hass) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + lambda entities: hass.async_create_task( + entity_platform.async_add_entities(entities) + ), + ) + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") + is not None + ) + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") + is not None + ) + cancel_coordinator() + + +SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_names={ + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("motion", None): True, + }, + entity_names={ + PassiveBluetoothEntityKey("motion", None): "Motion", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("motion", None): BinarySensorEntityDescription( + key="motion", + device_class=BinarySensorDeviceClass.MOTION, + ), + }, +) + + +async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start): + """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + binary_sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + sesnor_processor = PassiveBluetoothDataProcessor( + lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(binary_sensor_processor) + coordinator.async_register_processor(sesnor_processor) + cancel_coordinator = coordinator.async_start() + + binary_sensor_processor.async_add_listener(MagicMock()) + sesnor_processor.async_add_listener(MagicMock()) + + mock_add_sensor_entities = MagicMock() + mock_add_binary_sensor_entities = MagicMock() + + sesnor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_sensor_entities, + ) + binary_sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_binary_sensor_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_binary_sensor_entities.mock_calls) == 1 + assert len(mock_add_sensor_entities.mock_calls) == 1 + + binary_sesnor_entities = [ + *mock_add_binary_sensor_entities.mock_calls[0][1][0], + ] + sesnor_entities = [ + *mock_add_sensor_entities.mock_calls[0][1][0], + ] + + sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_entities[0] + sensor_entity_one.hass = hass + assert sensor_entity_one.available is True + assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" + assert sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="pressure", device_id=None + ) + + binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sesnor_entities[ + 0 + ] + binary_sensor_entity_one.hass = hass + assert binary_sensor_entity_one.available is True + assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" + assert binary_sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="motion", device_id=None + ) + cancel_coordinator() diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py new file mode 100644 index 00000000000..8e566a7ce5a --- /dev/null +++ b/tests/components/bluetooth/test_usage.py @@ -0,0 +1,25 @@ +"""Tests for the Bluetooth integration.""" + + +import bleak + +from homeassistant.components.bluetooth.models import HaBleakScannerWrapper +from homeassistant.components.bluetooth.usage import ( + install_multiple_bleak_catcher, + uninstall_multiple_bleak_catcher, +) + + +async def test_multiple_bleak_scanner_instances(hass): + """Test creating multiple BleakScanners without an integration.""" + install_multiple_bleak_catcher() + + instance = bleak.BleakScanner() + + assert isinstance(instance, HaBleakScannerWrapper) + + uninstall_multiple_bleak_catcher() + + instance = bleak.BleakScanner() + + assert not isinstance(instance, HaBleakScannerWrapper) diff --git a/tests/components/bluetooth_le_tracker/conftest.py b/tests/components/bluetooth_le_tracker/conftest.py new file mode 100644 index 00000000000..9fce8e85ea8 --- /dev/null +++ b/tests/components/bluetooth_le_tracker/conftest.py @@ -0,0 +1,8 @@ +"""Session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 308371c9aaa..f9f0a51fc0f 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,9 +1,18 @@ """Test Bluetooth LE device tracker.""" +import asyncio from datetime import timedelta from unittest.mock import patch +from bleak import BleakError +from bleak.backends.scanner import AdvertisementData, BLEDevice + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth_le_tracker import device_tracker +from homeassistant.components.bluetooth_le_tracker.device_tracker import ( + CONF_TRACK_BATTERY, + CONF_TRACK_BATTERY_INTERVAL, +) from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, @@ -16,7 +25,49 @@ from homeassistant.util import dt as dt_util, slugify from tests.common import async_fire_time_changed -async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): +class MockBleakClient: + """Mock BleakClient.""" + + def __init__(self, *args, **kwargs): + """Mock BleakClient.""" + pass + + async def __aenter__(self, *args, **kwargs): + """Mock BleakClient.__aenter__.""" + return self + + async def __aexit__(self, *args, **kwargs): + """Mock BleakClient.__aexit__.""" + pass + + +class MockBleakClientTimesOut(MockBleakClient): + """Mock BleakClient that times out.""" + + async def read_gatt_char(self, *args, **kwargs): + """Mock BleakClient.read_gatt_char.""" + raise asyncio.TimeoutError + + +class MockBleakClientFailing(MockBleakClient): + """Mock BleakClient that fails.""" + + async def read_gatt_char(self, *args, **kwargs): + """Mock BleakClient.read_gatt_char.""" + raise BleakError("Failed") + + +class MockBleakClientBattery5(MockBleakClient): + """Mock BleakClient that returns a battery level of 5.""" + + async def read_gatt_char(self, *args, **kwargs): + """Mock BleakClient.read_gatt_char.""" + return b"\x05" + + +async def test_preserve_new_tracked_device_name( + hass, mock_bluetooth, mock_device_tracker_conf +): """Test preserving tracked device name across new seens.""" address = "DE:AD:BE:EF:13:37" @@ -24,13 +75,24 @@ async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): entity_id = f"{DOMAIN}.{slugify(name)}" with patch( - "homeassistant.components." - "bluetooth_le_tracker.device_tracker.pygatt.GATTToolBackend" - ) as mock_backend, patch.object(device_tracker, "MIN_SEEN_NEW", 3): + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + device = BluetoothServiceInfoBleak( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), + ) # Return with name when seen first time - device = {"address": address, "name": name} - mock_backend.return_value.scan.return_value = [device] + mock_async_discovered_service_info.return_value = [device] config = { CONF_PLATFORM: "bluetooth_le_tracker", @@ -41,7 +103,19 @@ async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): assert result # Seen once here; return without name when seen subsequent times - device["name"] = None + device = BluetoothServiceInfoBleak( + name=None, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] # Tick until device seen enough times for to be registered for tracking for _ in range(device_tracker.MIN_SEEN_NEW - 1): @@ -54,3 +128,199 @@ async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): state = hass.states.get(entity_id) assert state assert state.name == name + + +async def test_tracking_battery_times_out( + hass, mock_bluetooth, mock_device_tracker_conf +): + """Test tracking the battery times out.""" + + address = "DE:AD:BE:EF:13:37" + name = "Mock device name" + entity_id = f"{DOMAIN}.{slugify(name)}" + + with patch( + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + + device = BluetoothServiceInfoBleak( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] + + config = { + CONF_PLATFORM: "bluetooth_le_tracker", + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_TRACK_BATTERY: True, + CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), + CONF_TRACK_NEW: True, + } + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + assert result + + # Tick until device seen enough times for to be registered for tracking + for _ in range(device_tracker.MIN_SEEN_NEW - 1): + async_fire_time_changed( + hass, + dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", + MockBleakClientTimesOut, + ): + # Wait for the battery scan + async_fire_time_changed( + hass, + dt_util.utcnow() + + config[CONF_SCAN_INTERVAL] + + timedelta(seconds=1) + + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.name == name + assert "battery" not in state.attributes + + +async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_conf): + """Test tracking the battery fails.""" + + address = "DE:AD:BE:EF:13:37" + name = "Mock device name" + entity_id = f"{DOMAIN}.{slugify(name)}" + + with patch( + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + + device = BluetoothServiceInfoBleak( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] + + config = { + CONF_PLATFORM: "bluetooth_le_tracker", + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_TRACK_BATTERY: True, + CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), + CONF_TRACK_NEW: True, + } + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + assert result + + # Tick until device seen enough times for to be registered for tracking + for _ in range(device_tracker.MIN_SEEN_NEW - 1): + async_fire_time_changed( + hass, + dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", + MockBleakClientFailing, + ): + # Wait for the battery scan + async_fire_time_changed( + hass, + dt_util.utcnow() + + config[CONF_SCAN_INTERVAL] + + timedelta(seconds=1) + + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.name == name + assert "battery" not in state.attributes + + +async def test_tracking_battery_successful( + hass, mock_bluetooth, mock_device_tracker_conf +): + """Test tracking the battery gets a value.""" + + address = "DE:AD:BE:EF:13:37" + name = "Mock device name" + entity_id = f"{DOMAIN}.{slugify(name)}" + + with patch( + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + + device = BluetoothServiceInfoBleak( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] + + config = { + CONF_PLATFORM: "bluetooth_le_tracker", + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_TRACK_BATTERY: True, + CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), + CONF_TRACK_NEW: True, + } + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + assert result + + # Tick until device seen enough times for to be registered for tracking + for _ in range(device_tracker.MIN_SEEN_NEW - 1): + async_fire_time_changed( + hass, + dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", + MockBleakClientBattery5, + ): + # Wait for the battery scan + async_fire_time_changed( + hass, + dt_util.utcnow() + + config[CONF_SCAN_INTERVAL] + + timedelta(seconds=1) + + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.name == name + assert state.attributes["battery"] == 5 diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 10178c22de8..3f22f984a54 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -35,7 +35,7 @@ async def test_show_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_connection_error(hass): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -75,7 +75,7 @@ async def test_full_user_flow_implementation(hass): context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] assert result2["data"] == FIXTURE_COMPLETE_ENTRY @@ -98,7 +98,7 @@ async def test_options_flow_implementation(hass): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "account_options" result = await hass.config_entries.options.async_configure( @@ -107,7 +107,7 @@ async def test_options_flow_implementation(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_READ_ONLY: True, } diff --git a/tests/components/bond/test_button.py b/tests/components/bond/test_button.py index 4411b25657b..24ea4730d6c 100644 --- a/tests/components/bond/test_button.py +++ b/tests/components/bond/test_button.py @@ -30,6 +30,7 @@ def light_brightness_increase_decrease_only(name: str): "actions": [ Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF, + Action.START_DIMMER, Action.START_INCREASING_BRIGHTNESS, Action.START_DECREASING_BRIGHTNESS, Action.STOP, @@ -75,6 +76,8 @@ async def test_entity_registry(hass: core.HomeAssistant): assert entity.unique_id == "test-hub-id_test-device-id_startincreasingbrightness" entity = registry.entities["button.name_1_start_decreasing_brightness"] assert entity.unique_id == "test-hub-id_test-device-id_startdecreasingbrightness" + entity = registry.entities["button.name_1_start_dimmer"] + assert entity.unique_id == "test-hub-id_test-device-id_startdimmer" async def test_mutually_exclusive_actions(hass: core.HomeAssistant): diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 16d76c5d753..f61a8d312b2 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -38,7 +38,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -88,7 +88,7 @@ async def test_authorize_no_ip_control(hass): DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_ip_control" @@ -117,7 +117,7 @@ async def test_duplicate_error(hass): result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -135,14 +135,14 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -168,14 +168,14 @@ async def test_create_entry_with_ipv6_address(hass): data={CONF_HOST: "2001:db8::1428:57ab"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -214,7 +214,7 @@ async def test_options_flow(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( @@ -222,5 +222,5 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_IGNORED_SOURCES: ["HDMI 1", "HDMI 2"]} diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 00493011500..60f29dbd901 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -21,7 +21,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -37,7 +37,7 @@ async def test_create_entry_with_hostname(hass): data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "example.local" assert result["data"][CONF_TYPE] == "laser" @@ -53,7 +53,7 @@ async def test_create_entry_with_ipv4_address(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" @@ -71,7 +71,7 @@ async def test_create_entry_with_ipv6_address(hass): data={CONF_HOST: "2001:db8::1428:57ab", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "2001:db8::1428:57ab" assert result["data"][CONF_TYPE] == "laser" @@ -116,7 +116,7 @@ async def test_unsupported_model_error(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unsupported_model" @@ -133,7 +133,7 @@ async def test_device_exists_abort(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_zeroconf_snmp_error(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -176,7 +176,7 @@ async def test_zeroconf_unsupported_model(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unsupported_model" assert len(mock_get_data.mock_calls) == 0 @@ -208,7 +208,7 @@ async def test_zeroconf_device_exists_abort(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -235,7 +235,7 @@ async def test_zeroconf_no_probe_existing_device(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_get_data.mock_calls) == 0 @@ -264,13 +264,13 @@ async def test_zeroconf_confirm_create_entry(hass): assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"]["model"] == "HL-L2340DW" assert result["description_placeholders"]["serial_number"] == "0123456789" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TYPE: "laser"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py index 4a949911ca6..b2eea79d463 100644 --- a/tests/components/brunt/test_config_flow.py +++ b/tests/components/brunt/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -57,7 +57,7 @@ async def test_form_duplicate_login(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -80,17 +80,17 @@ async def test_form_error(hass, side_effect, error_message): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error_message} @pytest.mark.parametrize( "side_effect, result_type, password, step_id, reason", [ - (None, data_entry_flow.RESULT_TYPE_ABORT, "test", None, "reauth_successful"), + (None, data_entry_flow.FlowResultType.ABORT, "test", None, "reauth_successful"), ( Exception, - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, CONFIG[CONF_PASSWORD], "reauth_confirm", None, @@ -115,7 +115,7 @@ async def test_reauth(hass, side_effect, result_type, password, step_id, reason) }, data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index 38485fb7959..b8efa960fca 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -28,7 +28,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_connection_error( @@ -54,7 +54,7 @@ async def test_connection_error( assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_user_device_exists_abort( @@ -75,7 +75,7 @@ async def test_user_device_exists_abort( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_full_user_flow_implementation( @@ -94,7 +94,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -114,7 +114,7 @@ async def test_full_user_flow_implementation( assert result["data"][CONF_PORT] == 80 assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127" assert result["title"] == "RVS21.831F/127" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "RVS21.831F/127" @@ -136,7 +136,7 @@ async def test_full_user_flow_implementation_without_auth( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -153,7 +153,7 @@ async def test_full_user_flow_implementation_without_auth( assert result["data"][CONF_PORT] == 80 assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127" assert result["title"] == "RVS21.831F/127" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "RVS21.831F/127" diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py index 828101bf77e..420e89b5bc2 100644 --- a/tests/components/buienradar/test_config_flow.py +++ b/tests/components/buienradar/test_config_flow.py @@ -96,7 +96,7 @@ async def test_options_flow(hass): ), patch( "homeassistant.components.buienradar.async_unload_entry", return_value=True ): - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 0a91f58b0b2..97bfd89f465 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -1,8 +1,10 @@ """The tests for the calendar component.""" from datetime import timedelta from http import HTTPStatus +from unittest.mock import patch from homeassistant.bootstrap import async_setup_component +from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util @@ -11,8 +13,6 @@ async def test_events_http_api(hass, hass_client): await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) await hass.async_block_till_done() client = await hass_client() - response = await client.get("/api/calendars/calendar.calendar_2") - assert response.status == HTTPStatus.BAD_REQUEST start = dt_util.now() end = start + timedelta(days=1) response = await client.get( @@ -25,6 +25,36 @@ async def test_events_http_api(hass, hass_client): assert events[0]["summary"] == "Future Event" +async def test_events_http_api_missing_fields(hass, hass_client): + """Test the calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + client = await hass_client() + response = await client.get("/api/calendars/calendar.calendar_2") + assert response.status == HTTPStatus.BAD_REQUEST + + +async def test_events_http_api_error(hass, hass_client): + """Test the calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + client = await hass_client() + start = dt_util.now() + end = start + timedelta(days=1) + + with patch( + "homeassistant.components.demo.calendar.DemoCalendar.async_get_events", + side_effect=HomeAssistantError("Failure"), + ): + response = await client.get( + "/api/calendars/calendar.calendar_1?start={}&end={}".format( + start.isoformat(), end.isoformat() + ) + ) + assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR + assert await response.json() == {"message": "Error reading events: Failure"} + + async def test_calendars_http_api(hass, hass_client): """Test the calendar demo view.""" await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) diff --git a/tests/components/calendar/test_recorder.py b/tests/components/calendar/test_recorder.py new file mode 100644 index 00000000000..0fbcaf38432 --- /dev/null +++ b/tests/components/calendar/test_recorder.py @@ -0,0 +1,44 @@ +"""The tests for calendar recorder.""" + +from datetime import timedelta + +from homeassistant.components.recorder.db_schema import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.recorder.common import async_wait_recording_done + + +async def test_events_http_api(hass, recorder_mock): + """Test the calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + + state = hass.states.get("calendar.calendar_1") + assert state + assert ATTR_FRIENDLY_NAME in state.attributes + assert "description" in state.attributes + + # calendar.calendar_1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_FRIENDLY_NAME in state.attributes + assert "description" not in state.attributes diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index a3b5bacca59..ebe5e9185e4 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -8,11 +8,11 @@ forward exercising the triggers. """ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Generator import datetime import logging import secrets -from typing import Any, Generator +from typing import Any from unittest.mock import patch import pytest diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index ba13bbd6c52..f800a4ac2bd 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,6 +1,5 @@ """The tests for the camera component.""" import asyncio -import base64 from http import HTTPStatus import io from unittest.mock import AsyncMock, Mock, PropertyMock, mock_open, patch @@ -235,24 +234,6 @@ async def test_snapshot_service(hass, mock_camera): assert mock_write.mock_calls[0][1][0] == b"Test" -async def test_websocket_camera_thumbnail(hass, hass_ws_client, mock_camera): - """Test camera_thumbnail websocket command.""" - await async_setup_component(hass, "camera", {}) - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "camera_thumbnail", "entity_id": "camera.demo_camera"} - ) - - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"]["content_type"] == "image/jpg" - assert msg["result"]["content"] == base64.b64encode(b"Test").decode("utf-8") - - async def test_websocket_stream_no_source( hass, hass_ws_client, mock_camera, mock_stream ): diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index b37c81407f7..2b1e4e9cd2e 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.canary.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_TIMEOUT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import USER_INPUT, _patch_async_setup, _patch_async_setup_entry, init_integration @@ -26,7 +22,7 @@ async def test_user_form(hass, canary_config_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: @@ -36,7 +32,7 @@ async def test_user_form(hass, canary_config_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**USER_INPUT, CONF_TIMEOUT: DEFAULT_TIMEOUT} @@ -57,7 +53,7 @@ async def test_user_form_cannot_connect(hass, canary_config_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} canary_config_flow.side_effect = ConnectTimeout() @@ -67,7 +63,7 @@ async def test_user_form_cannot_connect(hass, canary_config_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -84,7 +80,7 @@ async def test_user_form_unexpected_exception(hass, canary_config_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -97,7 +93,7 @@ async def test_user_form_single_instance_allowed(hass, canary_config_flow): context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -110,7 +106,7 @@ async def test_options_flow(hass, canary): assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with _patch_async_setup(), _patch_async_setup_entry(): @@ -120,6 +116,6 @@ async def test_options_flow(hass, canary): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "-v" assert result["data"][CONF_TIMEOUT] == 7 diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index d7aa0fdeda9..97218a396dd 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -24,10 +24,10 @@ async def test_creating_entry_sets_up_media_player(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_option_flow(hass, parameter_data): # Test ignore_cec and uuid options are hidden if advanced options are disabled result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema assert set(data_schema) == {"known_hosts"} @@ -201,7 +201,7 @@ async def test_option_flow(hass, parameter_data): result = await hass.config_entries.options.async_init( config_entry.entry_id, context=context ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema for other_param in basic_parameters: @@ -218,7 +218,7 @@ async def test_option_flow(hass, parameter_data): result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "advanced_options" for other_param in basic_parameters: if other_param == parameter: @@ -243,7 +243,7 @@ async def test_option_flow(hass, parameter_data): result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] is None for other_param in advanced_parameters: if other_param == parameter: @@ -257,7 +257,7 @@ async def test_option_flow(hass, parameter_data): result["flow_id"], user_input={"known_hosts": ""}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] is None expected_data = {**orig_data, "known_hosts": []} if parameter in advanced_parameters: diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index ed575c0fa22..9d3e1a6d534 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -311,7 +311,7 @@ async def test_internal_discovery_callback_fill_out_group_fail( await hass.async_block_till_done() # when called with incomplete info, it should use HTTP to get missing - discover = signal.mock_calls[0][1][0] + discover = signal.mock_calls[-1][1][0] assert discover == full_info get_multizone_status_mock.assert_called_once() @@ -352,7 +352,7 @@ async def test_internal_discovery_callback_fill_out_group( await hass.async_block_till_done() # when called with incomplete info, it should use HTTP to get missing - discover = signal.mock_calls[0][1][0] + discover = signal.mock_calls[-1][1][0] assert discover == full_info get_multizone_status_mock.assert_called_once() @@ -423,23 +423,25 @@ async def test_internal_discovery_callback_fill_out_cast_type_manufacturer( # when called with incomplete info, it should use HTTP to get missing get_cast_type_mock.assert_called_once() assert get_cast_type_mock.call_count == 1 - discover = signal.mock_calls[0][1][0] + discover = signal.mock_calls[2][1][0] assert discover == full_info assert "Fetched cast details for unknown model 'Chromecast'" in caplog.text + signal.reset_mock() # Call again, the model name should be fetched from cache discover_cast(FAKE_MDNS_SERVICE, info) await hass.async_block_till_done() assert get_cast_type_mock.call_count == 1 # No additional calls - discover = signal.mock_calls[1][1][0] + discover = signal.mock_calls[0][1][0] assert discover == full_info + signal.reset_mock() # Call for another model, need to call HTTP again get_cast_type_mock.return_value = full_info2.cast_info discover_cast(FAKE_MDNS_SERVICE, info2) await hass.async_block_till_done() assert get_cast_type_mock.call_count == 2 - discover = signal.mock_calls[2][1][0] + discover = signal.mock_calls[0][1][0] assert discover == full_info2 diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index a1cd1367e5f..4c6409b56bd 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -18,7 +18,7 @@ async def test_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -27,7 +27,7 @@ async def test_user(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -42,7 +42,7 @@ async def test_user_with_bad_cert(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -53,7 +53,7 @@ async def test_user_with_bad_cert(hass): result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -78,7 +78,7 @@ async def test_import_host_only(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT @@ -100,7 +100,7 @@ async def test_import_host_and_port(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -122,7 +122,7 @@ async def test_import_non_default_port(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{HOST}:888" assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == 888 @@ -144,7 +144,7 @@ async def test_import_with_name(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -163,7 +163,7 @@ async def test_bad_import(hass): data={CONF_HOST: HOST}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "import_failed" @@ -180,7 +180,7 @@ async def test_abort_if_already_setup(hass): context={"source": config_entries.SOURCE_IMPORT}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -188,7 +188,7 @@ async def test_abort_if_already_setup(hass): context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -205,7 +205,7 @@ async def test_abort_on_socket_failed(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "resolve_failed"} with patch( @@ -215,7 +215,7 @@ async def test_abort_on_socket_failed(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_timeout"} with patch( @@ -225,5 +225,5 @@ async def test_abort_on_socket_failed(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_refused"} diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py index 9aa16b8a7c5..69bcf5f9819 100644 --- a/tests/components/climacell/test_config_flow.py +++ b/tests/components/climacell/test_config_flow.py @@ -33,14 +33,14 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMESTEP: 1} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMESTEP] == 1 assert entry.options[CONF_TIMESTEP] == 1 diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index af1b14299ae..a6839043e62 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, + ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_PRESET_MODE, ATTR_SWING_MODE, @@ -14,6 +15,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, SERVICE_SET_AUX_HEAT, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, @@ -99,6 +101,7 @@ async def test_state_with_context(hass): (SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT), (SERVICE_SET_PRESET_MODE, ATTR_PRESET_MODE), (SERVICE_SET_SWING_MODE, ATTR_SWING_MODE), + (SERVICE_SET_FAN_MODE, ATTR_FAN_MODE), (SERVICE_SET_HUMIDITY, ATTR_HUMIDITY), (SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE), (SERVICE_SET_TEMPERATURE, ATTR_TARGET_TEMP_HIGH), diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index c1022dc6aae..ad914436c07 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -11,7 +11,7 @@ from homeassistant.components.cloud import account_link from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, mock_platform +from tests.common import MockConfigEntry, async_fire_time_changed, mock_platform TEST_DOMAIN = "oauth2_test" @@ -38,6 +38,18 @@ def flow_handler(hass): async def test_setup_provide_implementation(hass): """Test that we provide implementations.""" + legacy_entry = MockConfigEntry( + domain="legacy", + version=1, + data={"auth_implementation": "cloud"}, + ) + none_cloud_entry = MockConfigEntry( + domain="no_cloud", + version=1, + data={"auth_implementation": "somethingelse"}, + ) + none_cloud_entry.add_to_hass(hass) + legacy_entry.add_to_hass(hass) account_link.async_setup(hass) with patch( @@ -45,6 +57,21 @@ async def test_setup_provide_implementation(hass): return_value=[ {"service": "test", "min_version": "0.1.0"}, {"service": "too_new", "min_version": "1000000.0.0"}, + { + "service": "deprecated", + "min_version": "0.1.0", + "accepts_new_authorizations": False, + }, + { + "service": "legacy", + "min_version": "0.1.0", + "accepts_new_authorizations": False, + }, + { + "service": "no_cloud", + "min_version": "0.1.0", + "accepts_new_authorizations": False, + }, ], ): assert ( @@ -57,15 +84,33 @@ async def test_setup_provide_implementation(hass): await config_entry_oauth2_flow.async_get_implementations(hass, "too_new") == {} ) + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, "deprecated") + == {} + ) + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, "no_cloud") + == {} + ) + implementations = await config_entry_oauth2_flow.async_get_implementations( hass, "test" ) + legacy_implementations = ( + await config_entry_oauth2_flow.async_get_implementations(hass, "legacy") + ) + assert "cloud" in implementations assert implementations["cloud"].domain == "cloud" assert implementations["cloud"].service == "test" assert implementations["cloud"].hass is hass + assert "cloud" in legacy_implementations + assert legacy_implementations["cloud"].domain == "cloud" + assert legacy_implementations["cloud"].service == "legacy" + assert legacy_implementations["cloud"].hass is hass + async def test_get_services_cached(hass): """Test that we cache services.""" @@ -132,7 +177,7 @@ async def test_implementation(hass, flow_handler, current_request_with_host): TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == "http://example.com/auth" flow_finished.set_result( diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index df8cff0f3a4..3d225e75803 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -8,11 +8,7 @@ from pycfdns.exceptions import ( from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE, CONF_ZONE -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( ENTRY_CONFIG, @@ -31,7 +27,7 @@ async def test_user_form(hass, cfupdate_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -41,7 +37,7 @@ async def test_user_form(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "zone" assert result["errors"] == {} @@ -51,7 +47,7 @@ async def test_user_form(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "records" assert result["errors"] is None @@ -62,7 +58,7 @@ async def test_user_form(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_ZONE[CONF_ZONE] assert result["data"] @@ -90,7 +86,7 @@ async def test_user_form_cannot_connect(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -108,7 +104,7 @@ async def test_user_form_invalid_auth(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -126,7 +122,7 @@ async def test_user_form_invalid_zone(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_zone"} @@ -144,7 +140,7 @@ async def test_user_form_unexpected_exception(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -158,7 +154,7 @@ async def test_user_form_single_instance_allowed(hass): context={CONF_SOURCE: SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -176,7 +172,7 @@ async def test_reauth_flow(hass, cfupdate_flow): }, data=entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with _patch_async_setup_entry() as mock_setup_entry: @@ -186,7 +182,7 @@ async def test_reauth_flow(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_TOKEN] == "other_token" diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 7961e413135..d85279fc30c 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.co2signal import DOMAIN, config_flow from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import VALID_PAYLOAD @@ -17,7 +17,7 @@ async def test_form_home(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( @@ -33,7 +33,7 @@ async def test_form_home(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "CO2 Signal" assert result2["data"] == { "api_key": "api_key", @@ -47,7 +47,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -57,7 +57,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( "homeassistant.components.co2signal.async_setup_entry", @@ -72,7 +72,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "12.3, 45.6" assert result3["data"] == { "latitude": 12.3, @@ -88,7 +88,7 @@ async def test_form_country(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -98,7 +98,7 @@ async def test_form_country(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( "homeassistant.components.co2signal.async_setup_entry", @@ -112,7 +112,7 @@ async def test_form_country(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "fr" assert result3["data"] == { "country_code": "fr", @@ -147,7 +147,7 @@ async def test_form_error_handling(hass: HomeAssistant, err_str, err_code) -> No }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": err_code} @@ -169,7 +169,7 @@ async def test_form_error_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -191,5 +191,5 @@ async def test_form_error_unexpected_data(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index b523b02aa64..18f680fbb02 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from pytest import LogCaptureFixture + from homeassistant import setup from homeassistant.components.binary_sensor import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON @@ -112,3 +114,14 @@ async def test_unique_id(hass: HomeAssistant) -> None: ) is not None ) + + +async def test_return_code(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: + """Test setting the state with a template.""" + await setup_test_entity( + hass, + { + "command": "exit 33", + }, + ) + assert "return code 33" in caplog.text diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 98c917f51ba..deb80953428 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -155,7 +155,7 @@ async def test_reload(hass: HomeAssistant) -> None: async def test_move_cover_failure( caplog: LogCaptureFixture, hass: HomeAssistant ) -> None: - """Test with state value.""" + """Test command failure.""" await setup_test_entity( hass, @@ -165,6 +165,7 @@ async def test_move_cover_failure( DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True ) assert "Command failed" in caplog.text + assert "return code 1" in caplog.text async def test_unique_id(hass: HomeAssistant) -> None: diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 26b53a827e7..5cef13e45b4 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -77,6 +77,7 @@ async def test_error_for_none_zero_exit_code( DOMAIN, "test", {"message": "error"}, blocking=True ) assert "Command failed" in caplog.text + assert "return code 1" in caplog.text async def test_timeout(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 62ec1dbe97b..bdb36eebaa1 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -128,6 +128,17 @@ async def test_bad_command(hass: HomeAssistant) -> None: assert entity_state.state == "unknown" +async def test_return_code(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: + """Test that an error return code is logged.""" + await setup_test_entities( + hass, + { + "command": "exit 33", + }, + ) + assert "return code 33" in caplog.text + + async def test_update_with_json_attrs(hass: HomeAssistant) -> None: """Test attributes get extracted from a JSON result.""" await setup_test_entities( diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 307974ab3fe..267f7cf7b06 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -419,3 +419,16 @@ async def test_unique_id(hass: HomeAssistant) -> None: ent_reg.async_get_entity_id("switch", "command_line", "not-so-unique-anymore") is not None ) + + +async def test_command_failure(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: + """Test command failure.""" + + await setup_test_entity( + hass, + {"test": {"command_off": "exit 33"}}, + ) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True + ) + assert "return code 33" in caplog.text diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 611a7f75939..0de4bf44401 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1075,7 +1075,7 @@ async def test_ignore_flow(hass, hass_ws_client): result = await hass.config_entries.flow.async_init( "test", context={"source": core_ce.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM await ws_client.send_json( { diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index f923b326100..f9289b6e3b3 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -29,14 +29,14 @@ def registry(hass): async def test_list_devices(hass, client, registry): """Test list entries.""" - registry.async_get_or_create( + device1 = registry.async_get_or_create( config_entry_id="1234", connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) - registry.async_get_or_create( + device2 = registry.async_get_or_create( config_entry_id="1234", identifiers={("bridgeid", "1234")}, manufacturer="manufacturer", @@ -85,6 +85,32 @@ async def test_list_devices(hass, client, registry): }, ] + registry.async_remove_device(device2.id) + await hass.async_block_till_done() + + await client.send_json({"id": 6, "type": "config/device_registry/list"}) + msg = await client.receive_json() + + assert msg["result"] == [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": None, + "connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": device1.id, + "identifiers": [["bridgeid", "0123"]], + "manufacturer": "manufacturer", + "model": "model", + "name": None, + "name_by_user": None, + "sw_version": None, + "via_device_id": None, + } + ] + @pytest.mark.parametrize( "payload_key,payload_value", diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 69744817a27..e472736ee6c 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -5,6 +5,7 @@ from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON from homeassistant.helpers.device_registry import DeviceEntryDisabler from homeassistant.helpers.entity_registry import ( + EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, RegistryEntryDisabler, RegistryEntryHider, @@ -56,28 +57,68 @@ async def test_list_entities(hass, client): assert msg["result"] == [ { + "area_id": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "entity_id": "test_domain.name", - "hidden_by": None, - "name": "Hello World", - "icon": None, - "platform": "test_platform", "entity_category": None, + "entity_id": "test_domain.name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "name": "Hello World", + "original_name": None, + "platform": "test_platform", }, { + "area_id": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "entity_id": "test_domain.no_name", - "hidden_by": None, - "name": None, - "icon": None, - "platform": "test_platform", "entity_category": None, + "entity_id": "test_domain.no_name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "name": None, + "original_name": None, + "platform": "test_platform", + }, + ] + + mock_registry( + hass, + { + "test_domain.name": RegistryEntry( + entity_id="test_domain.name", + unique_id="1234", + platform="test_platform", + name="Hello World", + ), + }, + ) + + hass.bus.async_fire( + EVENT_ENTITY_REGISTRY_UPDATED, + {"action": "create", "entity_id": "test_domain.no_name"}, + ) + await client.send_json({"id": 6, "type": "config/entity_registry/list"}) + msg = await client.receive_json() + + assert msg["result"] == [ + { + "area_id": None, + "config_entry_id": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "name": "Hello World", + "original_name": None, + "platform": "test_platform", }, ] diff --git a/tests/components/conftest.py b/tests/components/conftest.py index f153263cbc6..6cad53aea72 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -19,8 +19,7 @@ def patch_zeroconf_multiple_catcher(): def prevent_io(): """Fixture to prevent certain I/O from happening.""" with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", - return_value=[], + "homeassistant.components.http.ban.load_yaml_config_file", ): yield diff --git a/tests/components/cpuspeed/test_config_flow.py b/tests/components/cpuspeed/test_config_flow.py index 8f12092f389..c016cfdb1d9 100644 --- a/tests/components/cpuspeed/test_config_flow.py +++ b/tests/components/cpuspeed/test_config_flow.py @@ -5,11 +5,7 @@ from unittest.mock import AsyncMock, MagicMock from homeassistant.components.cpuspeed.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -24,7 +20,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +29,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "CPU Speed" assert result2.get("data") == {} @@ -54,7 +50,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -71,7 +67,7 @@ async def test_not_compatible( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -81,7 +77,7 @@ async def test_not_compatible( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "not_compatible" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/cpuspeed/test_sensor.py b/tests/components/cpuspeed/test_sensor.py index ebf9f0111bd..068d24b6f3c 100644 --- a/tests/components/cpuspeed/test_sensor.py +++ b/tests/components/cpuspeed/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +from homeassistant.components.cpuspeed.const import DOMAIN from homeassistant.components.cpuspeed.sensor import ATTR_ARCH, ATTR_BRAND, ATTR_HZ from homeassistant.components.homeassistant import ( DOMAIN as HOME_ASSISTANT_DOMAIN, @@ -15,7 +16,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -29,6 +30,7 @@ async def test_sensor( """Test the CPU Speed sensor.""" await async_setup_component(hass, "homeassistant", {}) entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) entry = entity_registry.async_get("sensor.cpu_speed") assert entry @@ -62,6 +64,12 @@ async def test_sensor( assert state.attributes.get(ATTR_BRAND) == "Intel Ryzen 7" assert state.attributes.get(ATTR_HZ) == 3.6 + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, entry.config_entry_id)} + assert device_entry.name == "CPU Speed" + async def test_sensor_partial_info( hass: HomeAssistant, diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index fdc0df108ee..edc026c9304 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -183,7 +183,7 @@ async def test_no_user_input(crownstone_setup: MockFixture, hass: HomeAssistant) DOMAIN, context={"source": "user"} ) # show the login form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert crownstone_setup.call_count == 0 @@ -211,7 +211,7 @@ async def test_abort_if_configured(crownstone_setup: MockFixture, hass: HomeAssi result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # test if we abort if we try to configure the same entry - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert crownstone_setup.call_count == 0 @@ -228,7 +228,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} # side effect: auth error account not verified @@ -238,7 +238,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "account_not_verified"} assert crownstone_setup.call_count == 0 @@ -251,7 +251,7 @@ async def test_unknown_error(crownstone_setup: MockFixture, hass: HomeAssistant) result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown_error"} assert crownstone_setup.call_count == 0 @@ -271,14 +271,14 @@ async def test_successful_login_no_usb( result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # should show usb form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" # don't setup USB dongle, create entry result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: DONT_USE_USB} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_without_usb assert result["options"] == entry_options_without_usb assert crownstone_setup.call_count == 1 @@ -304,7 +304,7 @@ async def test_successful_login_with_usb( hass, get_mocked_crownstone_cloud(create_mocked_spheres(2)) ) # should show usb form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports_none_types.call_count == 1 @@ -324,7 +324,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports_none_types.call_count == 2 assert usb_path.call_count == 1 @@ -333,7 +333,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_usb assert result["options"] == entry_options_with_usb assert crownstone_setup.call_count == 1 @@ -356,7 +356,7 @@ async def test_successful_login_with_manual_usb_path( hass, get_mocked_crownstone_cloud(create_mocked_spheres(1)) ) # should show usb form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -365,7 +365,7 @@ async def test_successful_login_with_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -377,7 +377,7 @@ async def test_successful_login_with_manual_usb_path( # since we only have 1 sphere here, test that it's automatically selected and # creating entry without asking for user input - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_manual_usb assert result["options"] == entry_options_with_manual_usb assert crownstone_setup.call_count == 1 @@ -413,7 +413,7 @@ async def test_options_flow_setup_usb( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -427,7 +427,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -447,7 +447,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports.call_count == 2 assert usb_path.call_count == 1 @@ -456,7 +456,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_1" ) @@ -490,7 +490,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -507,7 +507,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant): CONF_USB_SPHERE_OPTION: "sphere_name_0", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=None, usb_sphere=None ) @@ -543,13 +543,13 @@ async def test_options_flow_manual_usb_path( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -558,7 +558,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -568,7 +568,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_MANUAL_PATH: path} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=path, usb_sphere="sphere_id_0" ) @@ -602,14 +602,14 @@ async def test_options_flow_change_usb_sphere(hass: HomeAssistant): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True, CONF_USB_SPHERE_OPTION: "sphere_name_2"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_2" ) diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 3a6a56fe097..bd118884edc 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components import zeroconf from homeassistant.components.daikin.const import KEY_MAC from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -54,7 +50,7 @@ async def test_user(hass, mock_daikin): context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -62,7 +58,7 @@ async def test_user(hass, mock_daikin): context={"source": SOURCE_USER}, data={CONF_HOST: HOST}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][KEY_MAC] == MAC @@ -77,7 +73,7 @@ async def test_abort_if_already_setup(hass, mock_daikin): data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -100,7 +96,7 @@ async def test_device_abort(hass, mock_daikin, s_effect, reason): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": reason} assert result["step_id"] == "user" @@ -112,7 +108,7 @@ async def test_api_password_abort(hass): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_API_KEY: "aa", CONF_PASSWORD: "aa"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "api_password"} assert result["step_id"] == "user" @@ -144,7 +140,7 @@ async def test_discovery_zeroconf( context={"source": source}, data=data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" MockConfigEntry(domain="daikin", unique_id=unique_id).add_to_hass(hass) @@ -154,7 +150,7 @@ async def test_discovery_zeroconf( data={CONF_HOST: HOST}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -163,5 +159,5 @@ async def test_discovery_zeroconf( data=data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 1a7031a0fd6..2f21081a5ae 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -29,11 +29,7 @@ from homeassistant.config_entries import ( SOURCE_USER, ) from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration @@ -55,14 +51,14 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -75,7 +71,7 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -100,7 +96,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): result["flow_id"], user_input={CONF_HOST: CONF_MANUAL_INPUT} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -108,7 +104,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -127,7 +123,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -148,7 +144,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -156,7 +152,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -175,7 +171,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -192,7 +188,7 @@ async def test_manual_configuration_after_discovery_timeout(hass, aioclient_mock DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -205,7 +201,7 @@ async def test_manual_configuration_after_discovery_ResponseError(hass, aioclien DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -224,7 +220,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -232,7 +228,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -251,7 +247,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" @@ -270,7 +266,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -278,7 +274,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -297,7 +293,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -313,7 +309,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -321,7 +317,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -338,7 +334,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_bridges" @@ -367,7 +363,7 @@ async def test_link_step_fails(hass, aioclient_mock, raised_error, error_string) result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post("http://1.2.3.4:80/api", exc=raised_error) @@ -376,7 +372,7 @@ async def test_link_step_fails(hass, aioclient_mock, raised_error, error_string) result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": error_string} @@ -391,7 +387,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): context={"source": SOURCE_REAUTH}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" new_api_key = "new_key" @@ -412,7 +408,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_API_KEY] == new_api_key @@ -433,7 +429,7 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): context={"source": SOURCE_SSDP}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" flows = hass.config_entries.flow.async_progress() @@ -450,7 +446,7 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -482,7 +478,7 @@ async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 @@ -506,7 +502,7 @@ async def test_ssdp_discovery_dont_update_configuration(hass, aioclient_mock): context={"source": SOURCE_SSDP}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -533,7 +529,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration( context={"source": SOURCE_SSDP}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -553,7 +549,7 @@ async def test_flow_hassio_discovery(hass): ), context={"source": SOURCE_HASSIO}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} @@ -572,7 +568,7 @@ async def test_flow_hassio_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["result"].data == { CONF_HOST: "mock-deconz", CONF_PORT: 80, @@ -603,7 +599,7 @@ async def test_hassio_discovery_update_configuration(hass, aioclient_mock): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert config_entry.data[CONF_PORT] == 8080 @@ -628,7 +624,7 @@ async def test_hassio_discovery_dont_update_configuration(hass, aioclient_mock): context={"source": SOURCE_HASSIO}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -638,7 +634,7 @@ async def test_option_flow(hass, aioclient_mock): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "deconz_devices" result = await hass.config_entries.options.async_configure( @@ -650,7 +646,7 @@ async def test_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_ALLOW_CLIP_SENSOR: False, CONF_ALLOW_DECONZ_GROUPS: False, diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index c252b00a228..0c37edc221d 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -42,68 +42,48 @@ async def test_cover(hass, aioclient_mock, mock_deconz_websocket): data = { "lights": { "1": { - "name": "Level controllable cover", - "type": "Level controllable output", - "state": {"bri": 254, "on": False, "reachable": True}, - "modelid": "Not zigbee spec", - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - "2": { "name": "Window covering device", "type": "Window covering device", "state": {"lift": 100, "open": False, "reachable": True}, "modelid": "lumi.curtain", "uniqueid": "00:00:00:00:00:00:00:01-00", }, - "3": { + "2": { "name": "Unsupported cover", "type": "Not a cover", "state": {"reachable": True}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, - "4": { - "name": "deconz old brightness cover", - "type": "Level controllable output", - "state": {"bri": 255, "on": False, "reachable": True}, - "modelid": "Not zigbee spec", - "uniqueid": "00:00:00:00:00:00:00:03-00", - }, - "5": { - "name": "Window covering controller", - "type": "Window covering controller", - "state": {"bri": 253, "on": True, "reachable": True}, - "modelid": "Motor controller", - "uniqueid": "00:00:00:00:00:00:00:04-00", - }, } } with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 5 - assert hass.states.get("cover.level_controllable_cover").state == STATE_OPEN - assert hass.states.get("cover.window_covering_device").state == STATE_CLOSED + assert len(hass.states.async_all()) == 2 + cover = hass.states.get("cover.window_covering_device") + assert cover.state == STATE_CLOSED + assert cover.attributes[ATTR_CURRENT_POSITION] == 0 assert not hass.states.get("cover.unsupported_cover") - assert hass.states.get("cover.deconz_old_brightness_cover").state == STATE_OPEN - assert hass.states.get("cover.window_covering_controller").state == STATE_CLOSED - # Event signals cover is closed + # Event signals cover is open event_changed_light = { "t": "event", "e": "changed", "r": "lights", "id": "1", - "state": {"on": True}, + "state": {"lift": 0, "open": True}, } await mock_deconz_websocket(data=event_changed_light) await hass.async_block_till_done() - assert hass.states.get("cover.level_controllable_cover").state == STATE_CLOSED + cover = hass.states.get("cover.window_covering_device") + assert cover.state == STATE_OPEN + assert cover.attributes[ATTR_CURRENT_POSITION] == 100 # Verify service calls for cover - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/2/state") + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") # Service open cover @@ -145,71 +125,10 @@ async def test_cover(hass, aioclient_mock, mock_deconz_websocket): ) assert aioclient_mock.mock_calls[4][2] == {"stop": True} - # Verify service calls for legacy cover - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") - - # Service open cover - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[5][2] == {"on": False} - - # Service close cover - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[6][2] == {"on": True} - - # Service set cover position - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 40}, - blocking=True, - ) - assert aioclient_mock.mock_calls[7][2] == {"bri": 152} - - # Service stop cover movement - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[8][2] == {"bri_inc": 0} - - # Test that a reported cover position of 255 (deconz-rest-api < 2.05.73) is interpreted correctly. - assert hass.states.get("cover.deconz_old_brightness_cover").state == STATE_OPEN - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "4", - "state": {"on": True}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - deconz_old_brightness_cover = hass.states.get("cover.deconz_old_brightness_cover") - assert deconz_old_brightness_cover.state == STATE_CLOSED - assert deconz_old_brightness_cover.attributes[ATTR_CURRENT_POSITION] == 0 - await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 5 + assert len(states) == 2 for state in states: assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 7701eb55b90..f8f8c20dbb2 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @@ -22,6 +23,7 @@ def recorder_url_mock(): yield -async def test_setup(hass, mock_zeroconf, mock_get_source_ip): +async def test_setup(hass, mock_zeroconf, mock_get_source_ip, mock_bluetooth): """Test setup.""" + recorder_helper.async_initialize_recorder(hass) assert await async_setup_component(hass, "default_config", {"foo": "bar"}) diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py index b56c717e635..a32b72704e8 100644 --- a/tests/components/deluge/test_config_flow.py +++ b/tests/components/deluge/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.deluge.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import CONF_DATA @@ -60,7 +56,7 @@ async def test_flow_user(hass: HomeAssistant, api): context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -78,7 +74,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant, api): DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -87,7 +83,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, conn_error): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -97,7 +93,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown_error): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -121,13 +117,13 @@ async def test_flow_reauth(hass: HomeAssistant, api): data=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index fa0aff8223b..5b322cb776f 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,6 +1,7 @@ """The tests for the Demo component.""" +from http import HTTPStatus import json -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest @@ -69,3 +70,138 @@ async def test_demo_statistics(hass, recorder_mock): "statistic_id": "demo:energy_consumption", "unit_of_measurement": "kWh", } in statistic_ids + + +async def test_issues_created(hass, hass_client, hass_ws_client): + """Test issues are created and can be fixed.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "transmogrifier_deprecated", + "issue_domain": None, + "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", + "severity": "warning", + "translation_key": "transmogrifier_deprecated", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": True, + "issue_id": "out_of_blinker_fluid", + "issue_domain": None, + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "out_of_blinker_fluid", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "unfixable_problem", + "issue_domain": None, + "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "severity": "warning", + "translation_key": "unfixable_problem", + "translation_placeholders": None, + }, + ] + } + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": "demo", "issue_id": "out_of_blinker_fluid"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "data_schema": [], + "description_placeholders": None, + "errors": None, + "flow_id": ANY, + "handler": "demo", + "last_step": None, + "step_id": "confirm", + "type": "form", + } + + url = f"/api/repairs/issues/fix/{flow_id}" + resp = await client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "description": None, + "description_placeholders": None, + "flow_id": flow_id, + "handler": "demo", + "title": "Fixed issue", + "type": "create_entry", + "version": 1, + } + + await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "transmogrifier_deprecated", + "issue_domain": None, + "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", + "severity": "warning", + "translation_key": "transmogrifier_deprecated", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "unfixable_problem", + "issue_domain": None, + "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "severity": "warning", + "translation_key": "unfixable_problem", + "translation_placeholders": None, + }, + ] + } diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index 91197927e95..39127f972fe 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -425,7 +425,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -433,7 +433,7 @@ async def test_options_flow(hass): user_input={CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, CONF_ZONE3: True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index 61ab7251f8a..b6fa62e290f 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.derivative.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My derivative" assert result["data"] == {} assert result["options"] == { @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -115,7 +115,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "unit_time": "h", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "My derivative", "round": 2.0, diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index d8914032f36..3bafa59fb96 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -162,7 +162,7 @@ async def test_duplicate_mac_dev_id(mock_warning, hass): assert "Duplicate device IDs" in args[0], "Duplicate device IDs warning expected" -async def test_setup_without_yaml_file(hass, enable_custom_integrations): +async def test_setup_without_yaml_file(hass, yaml_devices, enable_custom_integrations): """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index e9dae0b70b1..129c4a377ef 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -8,6 +8,9 @@ from devolo_home_control_api.homecontrol import HomeControl from devolo_home_control_api.properties.binary_sensor_property import ( BinarySensorProperty, ) +from devolo_home_control_api.properties.binary_switch_property import ( + BinarySwitchProperty, +) from devolo_home_control_api.properties.multi_level_sensor_property import ( MultiLevelSensorProperty, ) @@ -31,6 +34,15 @@ class BinarySensorPropertyMock(BinarySensorProperty): self.state = False +class BinarySwitchPropertyMock(BinarySwitchProperty): + """devolo Home Control binary sensor mock.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + self._logger = MagicMock() + self.element_uid = "Test" + + class MultiLevelSensorPropertyMock(MultiLevelSensorProperty): """devolo Home Control multi level sensor mock.""" @@ -134,6 +146,22 @@ class CoverMock(DeviceMock): } +class LightMock(DeviceMock): + """devolo Home Control light device mock.""" + + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.binary_switch_property = {} + self.multi_level_switch_property = { + "devolo.Dimmer:Test": MultiLevelSwitchPropertyMock() + } + self.multi_level_switch_property["devolo.Dimmer:Test"].switch_type = "dimmer" + self.multi_level_switch_property[ + "devolo.Dimmer:Test" + ].element_uid = "devolo.Dimmer:Test" + + class RemoteControlMock(DeviceMock): """devolo Home Control remote control device mock.""" @@ -219,6 +247,19 @@ class HomeControlMockCover(HomeControlMock): self.publisher.unregister = MagicMock() +class HomeControlMockLight(HomeControlMock): + """devolo Home Control gateway mock with light devices.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = { + "Test": LightMock(), + } + self.publisher = Publisher(self.devices.keys()) + self.publisher.unregister = MagicMock() + + class HomeControlMockRemoteControl(HomeControlMock): """devolo Home Control gateway mock with remote control device.""" diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index d2a359b9438..4f7140b7980 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -3,8 +3,10 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.devolo_home_control.const import DEFAULT_MYDEVOLO, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult, FlowResultType from .const import ( DISCOVERY_INFO, @@ -15,28 +17,28 @@ from .const import ( from tests.common import MockConfigEntry -async def test_form(hass): +async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} await _setup(hass, result) @pytest.mark.credentials_invalid -async def test_form_invalid_credentials_user(hass): +async def test_form_invalid_credentials_user(hass: HomeAssistant) -> None: """Test if we get the error message on invalid credentials.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -47,7 +49,7 @@ async def test_form_invalid_credentials_user(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_form_already_configured(hass): +async def test_form_already_configured(hass: HomeAssistant) -> None: """Test if we get the error message on already configured.""" with patch( "homeassistant.components.devolo_home_control.Mydevolo.uuid", @@ -59,17 +61,17 @@ async def test_form_already_configured(hass): context={"source": config_entries.SOURCE_USER}, data={"username": "test-username", "password": "test-password"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_form_advanced_options(hass): +async def test_form_advanced_options(hass: HomeAssistant) -> None: """Test if we get the advanced options if user has enabled it.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -100,7 +102,7 @@ async def test_form_advanced_options(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_zeroconf(hass): +async def test_form_zeroconf(hass: HomeAssistant) -> None: """Test that the zeroconf confirmation form is served.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -109,13 +111,13 @@ async def test_form_zeroconf(hass): ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM await _setup(hass, result) @pytest.mark.credentials_invalid -async def test_form_invalid_credentials_zeroconf(hass): +async def test_form_invalid_credentials_zeroconf(hass: HomeAssistant) -> None: """Test if we get the error message on invalid credentials.""" result = await hass.config_entries.flow.async_init( @@ -125,7 +127,7 @@ async def test_form_invalid_credentials_zeroconf(hass): ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -135,7 +137,7 @@ async def test_form_invalid_credentials_zeroconf(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_zeroconf_wrong_device(hass): +async def test_zeroconf_wrong_device(hass: HomeAssistant) -> None: """Test that the zeroconf ignores wrong devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -144,7 +146,7 @@ async def test_zeroconf_wrong_device(hass): ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT result = await hass.config_entries.flow.async_init( DOMAIN, @@ -153,10 +155,10 @@ async def test_zeroconf_wrong_device(hass): ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT -async def test_form_reauth(hass): +async def test_form_reauth(hass: HomeAssistant) -> None: """Test that the reauth confirmation form is served.""" mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) mock_config.add_to_hass(hass) @@ -174,7 +176,7 @@ async def test_form_reauth(hass): ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.devolo_home_control.async_setup_entry", @@ -189,12 +191,12 @@ async def test_form_reauth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.credentials_invalid -async def test_form_invalid_credentials_reauth(hass): +async def test_form_invalid_credentials_reauth(hass: HomeAssistant) -> None: """Test if we get the error message on invalid credentials.""" mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) mock_config.add_to_hass(hass) @@ -219,7 +221,7 @@ async def test_form_invalid_credentials_reauth(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_form_uuid_change_reauth(hass): +async def test_form_uuid_change_reauth(hass: HomeAssistant) -> None: """Test that the reauth confirmation form is served.""" mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) mock_config.add_to_hass(hass) @@ -237,7 +239,7 @@ async def test_form_uuid_change_reauth(hass): ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.devolo_home_control.async_setup_entry", @@ -252,11 +254,11 @@ async def test_form_uuid_change_reauth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "reauth_failed"} -async def _setup(hass, result): +async def _setup(hass: HomeAssistant, result: FlowResult) -> None: """Finish configuration steps.""" with patch( "homeassistant.components.devolo_home_control.async_setup_entry", diff --git a/tests/components/devolo_home_control/test_light.py b/tests/components/devolo_home_control/test_light.py new file mode 100644 index 00000000000..7b18b28a493 --- /dev/null +++ b/tests/components/devolo_home_control/test_light.py @@ -0,0 +1,165 @@ +"""Tests for the devolo Home Control light platform.""" +from unittest.mock import patch + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_SUPPORTED_COLOR_MODES, + DOMAIN, + ColorMode, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from . import configure_integration +from .mocks import BinarySwitchPropertyMock, HomeControlMock, HomeControlMockLight + + +async def test_light_without_binary_sensor(hass: HomeAssistant): + """Test setup and state change of a light device that does not have an additional binary sensor.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockLight() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] + assert state.attributes[ATTR_BRIGHTNESS] == round( + test_gateway.devices["Test"] + .multi_level_switch_property["devolo.Dimmer:Test"] + .value + / 100 + * 255 + ) + + # Emulate websocket message: brightness changed + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 0.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_OFF + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 100.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + + # Test setting brightness + with patch( + "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" + ) as set_value: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(100) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(0) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: f"{DOMAIN}.test", ATTR_BRIGHTNESS: 50}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(round(50 / 255 * 100)) + + # Emulate websocket message: device went offline + test_gateway.devices["Test"].status = 1 + test_gateway.publisher.dispatch("Test", ("Status", False, "status")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + + +async def test_light_with_binary_sensor(hass: HomeAssistant): + """Test setup and state change of a light device that has an additional binary sensor.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockLight() + test_gateway.devices["Test"].binary_switch_property = { + "devolo.BinarySwitch:Test": BinarySwitchPropertyMock() + } + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == STATE_ON + + # Emulate websocket message: brightness changed + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 0.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_OFF + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 100.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + + # Test setting brightness + with patch( + "devolo_home_control_api.properties.binary_switch_property.BinarySwitchProperty.set" + ) as set_value: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(True) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(False) + + +async def test_remove_from_hass(hass: HomeAssistant): + """Test removing entity.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockLight() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert test_gateway.publisher.unregister.call_count == 1 diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index 1c10d7a59ef..bb861081517 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -28,6 +28,8 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: async def async_connect(self, session_instance: Any = None): """Give a mocked device the needed properties.""" - self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] + self.product = DISCOVERY_INFO.properties["Product"] + self.serial_number = DISCOVERY_INFO.properties["SN"] diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index 516a19f3421..aec27ce6a8b 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -16,6 +16,10 @@ CONNECTED_STATIONS = { ], } +NO_CONNECTED_STATIONS = { + "connected_stations": [], +} + DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host=IP, addresses=[IP], diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 564e6e5ade6..8f9936be5bb 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -9,7 +9,12 @@ from homeassistant.components.devolo_home_network.const import ( CONNECTED_TO_ROUTER, LONG_UPDATE_INTERVAL, ) -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import EntityCategory @@ -25,10 +30,11 @@ from tests.common import async_fire_time_changed async def test_binary_sensor_setup(hass: HomeAssistant): """Test default setup of the binary sensor component.""" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.{CONNECTED_TO_ROUTER}") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}") is None await hass.config_entries.async_unload(entry.entry_id) @@ -36,8 +42,9 @@ async def test_binary_sensor_setup(hass: HomeAssistant): @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_attached_to_router(hass: HomeAssistant): """Test state change of a attached_to_router binary sensor device.""" - state_key = f"{DOMAIN}.{CONNECTED_TO_ROUTER}" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}" er = entity_registry.async_get(hass) @@ -47,6 +54,7 @@ async def test_update_attached_to_router(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected to router" assert er.async_get(state_key).entity_category == EntityCategory.DIAGNOSTIC diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 7cd1ba5222c..f9d589eb638 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -16,11 +16,7 @@ from homeassistant.components.devolo_home_network.const import ( ) from homeassistant.const import CONF_BASE, CONF_IP_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .const import DISCOVERY_INFO, DISCOVERY_INFO_WRONG_DEVICE, IP @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -45,7 +41,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["result"].unique_id == info["serial_number"] assert result2["title"] == info["title"] assert result2["data"] == { @@ -75,7 +71,7 @@ async def test_form_error(hass: HomeAssistant, exception_type, expected_error): }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {CONF_BASE: expected_error} @@ -88,7 +84,7 @@ async def test_zeroconf(hass: HomeAssistant): ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {"host_name": "test"} context = next( @@ -125,7 +121,7 @@ async def test_abort_zeroconf_wrong_device(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO_WRONG_DEVICE, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "home_control" @@ -158,7 +154,7 @@ async def test_abort_if_configued(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" # Abort on concurrent zeroconf discovery flow @@ -167,7 +163,7 @@ async def test_abort_if_configued(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO, ) - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" diff --git a/tests/components/devolo_home_network/test_device_tracker.py b/tests/components/devolo_home_network/test_device_tracker.py new file mode 100644 index 00000000000..233a480b5e3 --- /dev/null +++ b/tests/components/devolo_home_network/test_device_tracker.py @@ -0,0 +1,107 @@ +"""Tests for the devolo Home Network device tracker.""" +from unittest.mock import AsyncMock, patch + +from devolo_plc_api.exceptions.device import DeviceUnavailable +import pytest + +from homeassistant.components.device_tracker import DOMAIN as PLATFORM +from homeassistant.components.devolo_home_network.const import ( + DOMAIN, + LONG_UPDATE_INTERVAL, + WIFI_APTYPE, + WIFI_BANDS, +) +from homeassistant.const import ( + FREQUENCY_GIGAHERTZ, + STATE_HOME, + STATE_NOT_HOME, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.util import dt + +from . import configure_integration +from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NO_CONNECTED_STATIONS + +from tests.common import async_fire_time_changed + +STATION = CONNECTED_STATIONS["connected_stations"][0] +SERIAL = DISCOVERY_INFO.properties["SN"] + + +@pytest.mark.usefixtures("mock_device") +async def test_device_tracker(hass: HomeAssistant): + """Test device tracker states.""" + state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + entry = configure_integration(hass) + er = entity_registry.async_get(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + # Enable entity + er.async_update_entity(state_key, disabled_by=None) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_HOME + assert state.attributes["wifi"] == WIFI_APTYPE[STATION["vap_type"]] + assert ( + state.attributes["band"] + == f"{WIFI_BANDS[STATION['band']]} {FREQUENCY_GIGAHERTZ}" + ) + + # Emulate state change + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + new=AsyncMock(return_value=NO_CONNECTED_STATIONS), + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_NOT_HOME + + # Emulate device failure + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +async def test_restoring_clients(hass: HomeAssistant): + """Test restoring existing device_tracker entities.""" + state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + entry = configure_integration(hass) + er = entity_registry.async_get(hass) + er.async_get_or_create( + PLATFORM, + DOMAIN, + f"{SERIAL}_{STATION['mac_address']}", + config_entry=entry, + ) + + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + new=AsyncMock(return_value=NO_CONNECTED_STATIONS), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_NOT_HOME diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index 4f0c5b3fb58..1d15f337c17 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -12,7 +12,6 @@ from . import configure_integration @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_setup_entry(hass: HomeAssistant): """Test setup entry.""" entry = configure_integration(hass) @@ -24,7 +23,6 @@ async def test_setup_entry(hass: HomeAssistant): assert entry.state is ConfigEntryState.LOADED -@pytest.mark.usefixtures("mock_zeroconf") async def test_setup_device_not_found(hass: HomeAssistant): """Test setup entry.""" entry = configure_integration(hass) @@ -37,7 +35,6 @@ async def test_setup_device_not_found(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_unload_entry(hass: HomeAssistant): """Test unload entry.""" entry = configure_integration(hass) @@ -48,7 +45,6 @@ async def test_unload_entry(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_hass_stop(hass: HomeAssistant): """Test homeassistant stop event.""" entry = configure_integration(hass) diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index 582d6533802..33499f512fa 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.devolo_home_network.const import ( SHORT_UPDATE_INTERVAL, ) from homeassistant.components.sensor import DOMAIN, SensorStateClass -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import EntityCategory @@ -24,12 +24,13 @@ from tests.common import async_fire_time_changed async def test_sensor_setup(hass: HomeAssistant): """Test default setup of the sensor component.""" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.connected_wifi_clients") is not None - assert hass.states.get(f"{DOMAIN}.connected_plc_devices") is None - assert hass.states.get(f"{DOMAIN}.neighboring_wifi_networks") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_connected_wifi_clients") is not None + assert hass.states.get(f"{DOMAIN}.{device_name}_connected_plc_devices") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_neighboring_wifi_networks") is None await hass.config_entries.async_unload(entry.entry_id) @@ -37,15 +38,18 @@ async def test_sensor_setup(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") async def test_update_connected_wifi_clients(hass: HomeAssistant): """Test state change of a connected_wifi_clients sensor device.""" - state_key = f"{DOMAIN}.connected_wifi_clients" - entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_connected_wifi_clients" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected Wifi clients" + ) assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT # Emulate device failure @@ -74,8 +78,9 @@ async def test_update_connected_wifi_clients(hass: HomeAssistant): @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_neighboring_wifi_networks(hass: HomeAssistant): """Test state change of a neighboring_wifi_networks sensor device.""" - state_key = f"{DOMAIN}.neighboring_wifi_networks" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_neighboring_wifi_networks" er = entity_registry.async_get(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -83,6 +88,10 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] + == f"{entry.title} Neighboring Wifi networks" + ) assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure @@ -111,8 +120,9 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_connected_plc_devices(hass: HomeAssistant): """Test state change of a connected_plc_devices sensor device.""" - state_key = f"{DOMAIN}.connected_plc_devices" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_connected_plc_devices" er = entity_registry.async_get(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -120,6 +130,9 @@ async def test_update_connected_plc_devices(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected PLC devices" + ) assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index 48544ca0158..b20321277fb 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -33,7 +33,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIG[CONF_USERNAME] assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -54,7 +54,7 @@ async def test_form_account_error(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -73,7 +73,7 @@ async def test_form_session_error(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -92,7 +92,7 @@ async def test_form_unknown_error(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -107,14 +107,14 @@ async def test_option_flow_default(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_UNIT_OF_MEASUREMENT: MG_DL, } @@ -131,14 +131,14 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_UNIT_OF_MEASUREMENT: MMOL_L, } diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 5c4ef3e99f9..8bd185e8011 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -87,10 +87,10 @@ async def fixture(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "dialogflow", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] return await hass_client_no_auth(), webhook_id diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index f80e0d781ef..7ef6bdde69d 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_SERIAL from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.components.directv import ( HOST, @@ -35,7 +31,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_show_ssdp_form( @@ -49,7 +45,7 @@ async def test_show_ssdp_form( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -67,7 +63,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -85,7 +81,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -102,7 +98,7 @@ async def test_ssdp_confirm_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -119,7 +115,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -136,7 +132,7 @@ async def test_ssdp_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -154,7 +150,7 @@ async def test_ssdp_with_receiver_id_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -173,7 +169,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -192,7 +188,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -211,7 +207,7 @@ async def test_ssdp_confirm_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -226,7 +222,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -236,7 +232,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] @@ -255,7 +251,7 @@ async def test_full_ssdp_flow_implementation( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -263,7 +259,7 @@ async def test_full_ssdp_flow_implementation( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] diff --git a/tests/components/discord/test_config_flow.py b/tests/components/discord/test_config_flow.py index 9d4966929be..b6504e851ca 100644 --- a/tests/components/discord/test_config_flow.py +++ b/tests/components/discord/test_config_flow.py @@ -28,7 +28,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -45,7 +45,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -58,7 +58,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -67,7 +67,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -81,7 +81,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -90,7 +90,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -104,7 +104,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -113,7 +113,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -131,7 +131,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_conf = {CONF_API_TOKEN: "1234567890123"} @@ -141,7 +141,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -150,6 +150,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA | new_conf diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 7ec25906f99..8035b7ee822 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -84,7 +84,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -92,7 +92,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -118,7 +118,7 @@ async def test_user_flow_discovered_manual( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -126,7 +126,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -134,7 +134,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -158,7 +158,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -166,7 +166,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_NAME} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -188,7 +188,7 @@ async def test_user_flow_uncontactable( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -196,7 +196,7 @@ async def test_user_flow_uncontactable( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "manual" @@ -221,7 +221,7 @@ async def test_user_flow_embedded_st( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -229,7 +229,7 @@ async def test_user_flow_embedded_st( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -251,7 +251,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -259,7 +259,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "not_dmr"} assert result["step_id"] == "manual" @@ -271,7 +271,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -279,7 +279,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -302,7 +302,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError @@ -312,7 +312,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -342,7 +342,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -357,7 +357,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -382,7 +382,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -398,7 +398,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" # Service list does not contain services @@ -410,7 +410,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" # AVTransport service is missing @@ -426,7 +426,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -448,7 +448,7 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -462,7 +462,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "alternative_integration" discovery = dataclasses.replace(MOCK_DISCOVERY) @@ -475,7 +475,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "alternative_integration" for manufacturer, model in [ @@ -493,7 +493,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "alternative_integration" @@ -507,7 +507,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == {} @@ -526,7 +526,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -534,7 +534,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -559,7 +559,7 @@ async def test_unignore_flow_offline( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == {} @@ -572,7 +572,7 @@ async def test_unignore_flow_offline( context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "discovery_error" @@ -583,7 +583,7 @@ async def test_options_flow( config_entry_mock.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry_mock.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -597,7 +597,7 @@ async def test_options_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_url"} @@ -612,7 +612,7 @@ async def test_options_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LISTEN_PORT: 2222, CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py index 591457b23c6..1d6ac0eaf80 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -86,7 +86,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -95,7 +95,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -119,7 +119,7 @@ async def test_user_flow_no_devices( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -130,7 +130,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -138,7 +138,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -161,7 +161,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" upnp_factory_mock.async_create_device.side_effect = UpnpError @@ -171,7 +171,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -201,7 +201,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -216,7 +216,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -230,7 +230,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "bad_ssdp" # Missing USN @@ -240,7 +240,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "bad_ssdp" @@ -280,7 +280,7 @@ async def test_duplicate_name( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -288,7 +288,7 @@ async def test_duplicate_name( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: new_device_location, @@ -318,7 +318,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -334,7 +334,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" # Service list does not contain services @@ -346,7 +346,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" # ContentDirectory service is missing @@ -362,7 +362,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" @@ -384,5 +384,5 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 51e169b8bb5..fdec45be7f5 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -18,11 +18,7 @@ from homeassistant.components.dnsip.const import ( ) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -66,7 +62,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -108,7 +104,7 @@ async def test_form_adv(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -141,7 +137,7 @@ async def test_form_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_hostname"} @@ -183,7 +179,7 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -217,7 +213,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -228,7 +224,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "resolver": "8.8.8.8", "resolver_ipv6": "2001:4860:4860::8888", @@ -287,7 +283,7 @@ async def test_options_error(hass: HomeAssistant, p_input: dict[str, str]) -> No ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "init" if p_input[CONF_IPV4]: assert result2["errors"] == {"resolver": "invalid_resolver"} diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index cbf455653ff..f79a60e3265 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -43,7 +43,7 @@ async def test_user_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} doorbirdapi = _get_mock_doorbirdapi_return_values( @@ -188,7 +188,7 @@ async def test_form_zeroconf_correct_oui(hass): ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -251,7 +251,7 @@ async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_ ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_doorbird_device" @@ -271,7 +271,7 @@ async def test_form_user_cannot_connect(hass): VALID_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -312,12 +312,12 @@ async def test_options_flow(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_EVENTS: "eventa, eventc, eventq"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_EVENTS: ["eventa", "eventc", "eventq"]} diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index ddec7bda888..50fd1e4b7a8 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -562,7 +562,7 @@ async def test_options_flow(hass): with patch( "homeassistant.components.dsmr.async_setup_entry", return_value=True ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/dsmr/test_init.py b/tests/components/dsmr/test_init.py new file mode 100644 index 00000000000..914a9f6bdaf --- /dev/null +++ b/tests/components/dsmr/test_init.py @@ -0,0 +1,133 @@ +"""Tests for the DSMR integration.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.dsmr.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "dsmr_version,old_unique_id,new_unique_id", + [ + ("5", "1234_Power_Consumption", "1234_current_electricity_usage"), + ("5", "1234_Power_Production", "1234_current_electricity_delivery"), + ("5", "1234_Power_Tariff", "1234_electricity_active_tariff"), + ("5", "1234_Energy_Consumption_(tarif_1)", "1234_electricity_used_tariff_1"), + ("5", "1234_Energy_Consumption_(tarif_2)", "1234_electricity_used_tariff_2"), + ( + "5", + "1234_Energy_Production_(tarif_1)", + "1234_electricity_delivered_tariff_1", + ), + ( + "5", + "1234_Energy_Production_(tarif_2)", + "1234_electricity_delivered_tariff_2", + ), + ( + "5", + "1234_Power_Consumption_Phase_L1", + "1234_instantaneous_active_power_l1_positive", + ), + ( + "5", + "1234_Power_Consumption_Phase_L2", + "1234_instantaneous_active_power_l2_positive", + ), + ( + "5", + "1234_Power_Consumption_Phase_L3", + "1234_instantaneous_active_power_l3_positive", + ), + ( + "5", + "1234_Power_Production_Phase_L1", + "1234_instantaneous_active_power_l1_negative", + ), + ( + "5", + "1234_Power_Production_Phase_L2", + "1234_instantaneous_active_power_l2_negative", + ), + ( + "5", + "1234_Power_Production_Phase_L3", + "1234_instantaneous_active_power_l3_negative", + ), + ("5", "1234_Short_Power_Failure_Count", "1234_short_power_failure_count"), + ("5", "1234_Long_Power_Failure_Count", "1234_long_power_failure_count"), + ("5", "1234_Voltage_Sags_Phase_L1", "1234_voltage_sag_l1_count"), + ("5", "1234_Voltage_Sags_Phase_L2", "1234_voltage_sag_l2_count"), + ("5", "1234_Voltage_Sags_Phase_L3", "1234_voltage_sag_l3_count"), + ("5", "1234_Voltage_Swells_Phase_L1", "1234_voltage_swell_l1_count"), + ("5", "1234_Voltage_Swells_Phase_L2", "1234_voltage_swell_l2_count"), + ("5", "1234_Voltage_Swells_Phase_L3", "1234_voltage_swell_l3_count"), + ("5", "1234_Voltage_Phase_L1", "1234_instantaneous_voltage_l1"), + ("5", "1234_Voltage_Phase_L2", "1234_instantaneous_voltage_l2"), + ("5", "1234_Voltage_Phase_L3", "1234_instantaneous_voltage_l3"), + ("5", "1234_Current_Phase_L1", "1234_instantaneous_current_l1"), + ("5", "1234_Current_Phase_L2", "1234_instantaneous_current_l2"), + ("5", "1234_Current_Phase_L3", "1234_instantaneous_current_l3"), + ("5B", "1234_Max_power_per_phase", "1234_belgium_max_power_per_phase"), + ("5B", "1234_Max_current_per_phase", "1234_belgium_max_current_per_phase"), + ("5L", "1234_Energy_Consumption_(total)", "1234_electricity_imported_total"), + ("5L", "1234_Energy_Production_(total)", "1234_electricity_exported_total"), + ("5L", "1234_Energy_Production_(total)", "1234_electricity_exported_total"), + ("5", "1234_Gas_Consumption", "1234_hourly_gas_meter_reading"), + ("5B", "1234_Gas_Consumption", "1234_belgium_5min_gas_meter_reading"), + ("2.2", "1234_Gas_Consumption", "1234_gas_meter_reading"), + ], +) +async def test_migrate_unique_id( + hass: HomeAssistant, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], + dsmr_version: str, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test migration of unique_id.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": dsmr_version, + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="my_sensor", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) + is None + ) + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, new_unique_id) + == "sensor.my_sensor" + ) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 4502f61586a..e8cf763f98e 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -77,18 +77,18 @@ async def test_default_setup(hass, dsmr_connection_fixture): registry = er.async_get(hass) - entry = registry.async_get("sensor.power_consumption") + entry = registry.async_get("sensor.electricity_meter_power_consumption") assert entry - assert entry.unique_id == "1234_Power_Consumption" + assert entry.unique_id == "1234_current_electricity_usage" - entry = registry.async_get("sensor.gas_consumption") + entry = registry.async_get("sensor.gas_meter_gas_consumption") assert entry - assert entry.unique_id == "5678_Gas_Consumption" + assert entry.unique_id == "5678_gas_meter_reading" telegram_callback = connection_factory.call_args_list[0][0][2] # make sure entities have been created and return 'unknown' state - power_consumption = hass.states.get("sensor.power_consumption") + power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") assert power_consumption.state == STATE_UNKNOWN assert ( power_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER @@ -107,22 +107,22 @@ async def test_default_setup(hass, dsmr_connection_fixture): await asyncio.sleep(0) # ensure entities have new state value after incoming telegram - power_consumption = hass.states.get("sensor.power_consumption") + power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") assert power_consumption.state == "0.0" assert ( power_consumption.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR ) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -155,11 +155,11 @@ async def test_setup_only_energy(hass, dsmr_connection_fixture): registry = er.async_get(hass) - entry = registry.async_get("sensor.power_consumption") + entry = registry.async_get("sensor.electricity_meter_power_consumption") assert entry - assert entry.unique_id == "1234_Power_Consumption" + assert entry.unique_id == "1234_current_electricity_usage" - entry = registry.async_get("sensor.gas_consumption") + entry = registry.async_get("sensor.gas_meter_gas_consumption") assert not entry @@ -213,15 +213,15 @@ async def test_v4_meter(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS @@ -284,15 +284,15 @@ async def test_v5_meter(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -359,24 +359,24 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): # after receiving telegram entities need to have the chance to update await asyncio.sleep(0) - power_tariff = hass.states.get("sensor.energy_consumption_total") - assert power_tariff.state == "123.456" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert power_tariff.attributes.get(ATTR_ICON) is None + active_tariff = hass.states.get("sensor.electricity_meter_energy_consumption_total") + assert active_tariff.state == "123.456" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert active_tariff.attributes.get(ATTR_ICON) is None assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) - power_tariff = hass.states.get("sensor.energy_production_total") - assert power_tariff.state == "654.321" - assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") + assert active_tariff.state == "654.321" + assert active_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -438,15 +438,15 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "normal" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "normal" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -497,12 +497,12 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" async def test_swedish_meter(hass, dsmr_connection_fixture): @@ -553,26 +553,26 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): # after receiving telegram entities need to have the chance to update await asyncio.sleep(0) - power_tariff = hass.states.get("sensor.energy_consumption_total") - assert power_tariff.state == "123.456" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert power_tariff.attributes.get(ATTR_ICON) is None + active_tariff = hass.states.get("sensor.electricity_meter_energy_consumption_total") + assert active_tariff.state == "123.456" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert active_tariff.attributes.get(ATTR_ICON) is None assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) - power_tariff = hass.states.get("sensor.energy_production_total") - assert power_tariff.state == "654.321" + active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") + assert active_tariff.state == "654.321" assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) @@ -627,26 +627,26 @@ async def test_easymeter(hass, dsmr_connection_fixture): # after receiving telegram entities need to have the chance to update await asyncio.sleep(0) - power_tariff = hass.states.get("sensor.energy_consumption_total") - assert power_tariff.state == "54184.6316" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert power_tariff.attributes.get(ATTR_ICON) is None + active_tariff = hass.states.get("sensor.electricity_meter_energy_consumption_total") + assert active_tariff.state == "54184.6316" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert active_tariff.attributes.get(ATTR_ICON) is None assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) - power_tariff = hass.states.get("sensor.energy_production_total") - assert power_tariff.state == "19981.1069" + active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") + assert active_tariff.state == "19981.1069" assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index 76585ac73a1..9cc32a40371 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -74,7 +74,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_HOSTNAME ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "dunehd-host" assert result["data"] == {CONF_HOST: "dunehd-host"} @@ -90,6 +90,6 @@ async def test_create_entry_with_ipv6_address(hass): data={CONF_HOST: "2001:db8::1428:57ab"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "2001:db8::1428:57ab" assert result["data"] == {CONF_HOST: "2001:db8::1428:57ab"} diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 8311b4aba2c..0c80f20859f 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -25,7 +25,7 @@ async def test_abort_if_already_setup(hass): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -36,7 +36,7 @@ async def test_user_step_without_user_input(hass): flow.hass.data[DATA_ECOBEE_CONFIG] = {} result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -53,7 +53,7 @@ async def test_pin_request_succeeds(hass): result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -70,7 +70,7 @@ async def test_pin_request_fails(hass): result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "pin_request_failed" @@ -92,7 +92,7 @@ async def test_token_request_succeeds(hass): result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", @@ -116,7 +116,7 @@ async def test_token_request_fails(hass): result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" assert result["errors"]["base"] == "token_request_failed" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -131,7 +131,7 @@ async def test_import_flow_triggered_but_no_ecobee_conf(hass): result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -155,7 +155,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_t result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", diff --git a/tests/components/econet/test_config_flow.py b/tests/components/econet/test_config_flow.py index 68eb18a931e..635b9e2e40a 100644 --- a/tests/components/econet/test_config_flow.py +++ b/tests/components/econet/test_config_flow.py @@ -7,11 +7,7 @@ from pyeconet.errors import InvalidCredentialsError, PyeconetError from homeassistant.components.econet import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +18,7 @@ async def test_bad_credentials(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -39,7 +35,7 @@ async def test_bad_credentials(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "invalid_auth", @@ -52,7 +48,7 @@ async def test_generic_error_from_library(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -69,7 +65,7 @@ async def test_generic_error_from_library(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "cannot_connect", @@ -82,7 +78,7 @@ async def test_auth_worked(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -99,7 +95,7 @@ async def test_auth_worked(hass): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EMAIL: "admin@localhost.com", CONF_PASSWORD: "password0", @@ -119,7 +115,7 @@ async def test_already_configured(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -136,5 +132,5 @@ async def test_already_configured(hass): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/efergy/test_config_flow.py b/tests/components/efergy/test_config_flow.py index 89f3b266b7c..95effbfbd72 100644 --- a/tests/components/efergy/test_config_flow.py +++ b/tests/components/efergy/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.efergy.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import CONF_DATA, HID, _patch_efergy, _patch_efergy_status, create_entry @@ -27,14 +23,14 @@ async def test_flow_user(hass: HomeAssistant): DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == HID @@ -47,7 +43,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -59,7 +55,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -71,7 +67,7 @@ async def test_flow_user_unknown(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -90,7 +86,7 @@ async def test_flow_reauth(hass: HomeAssistant): data=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" new_conf = {CONF_API_KEY: "1234567890"} @@ -98,6 +94,6 @@ async def test_flow_reauth(hass: HomeAssistant): result["flow_id"], user_input=new_conf, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf diff --git a/tests/components/eight_sleep/test_config_flow.py b/tests/components/eight_sleep/test_config_flow.py index 8015fb6c69d..1cace5b31cd 100644 --- a/tests/components/eight_sleep/test_config_flow.py +++ b/tests/components/eight_sleep/test_config_flow.py @@ -1,11 +1,7 @@ """Test the Eight Sleep config flow.""" from homeassistant import config_entries from homeassistant.components.eight_sleep.const import DOMAIN -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass) -> None: @@ -13,7 +9,7 @@ async def test_form(hass) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -24,7 +20,7 @@ async def test_form(hass) -> None: }, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -37,7 +33,7 @@ async def test_form_invalid_auth(hass, token_error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -48,7 +44,7 @@ async def test_form_invalid_auth(hass, token_error) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -63,7 +59,7 @@ async def test_import(hass) -> None: }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == { "username": "test-username", @@ -81,5 +77,5 @@ async def test_import_invalid_auth(hass, token_error) -> None: "password": "bad-password", }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/elgato/test_button.py b/tests/components/elgato/test_button.py index 2ab3d7ee7c4..1f673bc6f44 100644 --- a/tests/components/elgato/test_button.py +++ b/tests/components/elgato/test_button.py @@ -25,12 +25,12 @@ async def test_button_identify( device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) - state = hass.states.get("button.identify") + state = hass.states.get("button.frenck_identify") assert state assert state.attributes.get(ATTR_ICON) == "mdi:help" assert state.state == STATE_UNKNOWN - entry = entity_registry.async_get("button.identify") + entry = entity_registry.async_get("button.frenck_identify") assert entry assert entry.unique_id == "CN11A1A00001_identify" assert entry.entity_category == EntityCategory.CONFIG @@ -53,14 +53,14 @@ async def test_button_identify( await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.identify"}, + {ATTR_ENTITY_ID: "button.frenck_identify"}, blocking=True, ) assert len(mock_elgato.identify.mock_calls) == 1 mock_elgato.identify.assert_called_with() - state = hass.states.get("button.identify") + state = hass.states.get("button.frenck_identify") assert state assert state.state == "2021-11-13T11:48:00+00:00" @@ -79,7 +79,7 @@ async def test_button_identify_error( await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.identify"}, + {ATTR_ENTITY_ID: "button.frenck_identify"}, blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index 0e3916a005e..dbdfbfed1d0 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.elgato.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -28,7 +24,7 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -36,7 +32,7 @@ async def test_full_user_flow_implementation( result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 9123} ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "CN11A1A00001" assert result2.get("data") == { CONF_HOST: "127.0.0.1", @@ -72,7 +68,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {"serial_number": "CN11A1A00001"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result progress = hass.config_entries.flow.async_progress() @@ -85,7 +81,7 @@ async def test_full_zeroconf_flow_implementation( result["flow_id"], user_input={} ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "CN11A1A00001" assert result2.get("data") == { CONF_HOST: "127.0.0.1", @@ -111,7 +107,7 @@ async def test_connection_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert result.get("step_id") == "user" @@ -137,7 +133,7 @@ async def test_zeroconf_connection_error( ) assert result.get("reason") == "cannot_connect" - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT async def test_user_device_exists_abort( @@ -153,7 +149,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -178,7 +174,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -199,7 +195,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -227,7 +223,7 @@ async def test_zeroconf_during_onboarding( ), ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "CN11A1A00001" assert result.get("data") == { CONF_HOST: "127.0.0.1", diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 398dfd30d03..e47dc402b64 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.elkm1.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( ELK_DISCOVERY, @@ -47,7 +47,7 @@ async def test_discovery_ignored_entry(hass): data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -242,7 +242,7 @@ async def test_form_user_with_insecure_elk_times_out(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -280,7 +280,7 @@ async def test_form_user_with_secure_elk_no_discovery_ip_already_configured(hass ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "address_already_configured" @@ -961,7 +961,7 @@ async def test_form_import_existing(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "address_already_configured" @@ -989,7 +989,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == "cc:cc:cc:cc:cc:cc" @@ -1018,7 +1018,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MOCK_MAC @@ -1034,7 +1034,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(), _patch_elk(): @@ -1044,7 +1044,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_elk(): @@ -1058,7 +1058,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -1073,7 +1073,7 @@ async def test_discovered_by_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1118,7 +1118,7 @@ async def test_discovered_by_discovery_non_standard_port(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1169,7 +1169,7 @@ async def test_discovered_by_discovery_url_already_configured(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -1182,7 +1182,7 @@ async def test_discovered_by_dhcp_udp_responds(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1225,7 +1225,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1275,7 +1275,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1315,5 +1315,5 @@ async def test_discovered_by_dhcp_no_udp_response(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py index 5b8d42799e9..e4e9889aadd 100644 --- a/tests/components/elmax/test_config_flow.py +++ b/tests/components/elmax/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -60,7 +60,7 @@ async def test_standard_setup(hass): }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_one_config_allowed(hass): @@ -94,7 +94,7 @@ async def test_one_config_allowed(hass): CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -115,7 +115,7 @@ async def test_invalid_credentials(hass): }, ) assert login_result["step_id"] == "user" - assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "invalid_auth"} @@ -136,7 +136,7 @@ async def test_connection_error(hass): }, ) assert login_result["step_id"] == "user" - assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "network_error"} @@ -164,7 +164,7 @@ async def test_unhandled_error(hass): }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -193,7 +193,7 @@ async def test_invalid_pin(hass): }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -215,7 +215,7 @@ async def test_no_online_panel(hass): }, ) assert login_result["step_id"] == "user" - assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "no_panel_online"} @@ -231,7 +231,7 @@ async def test_show_reauth(hass): CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -272,7 +272,7 @@ async def test_reauth_flow(hass): CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() assert result["reason"] == "reauth_successful" @@ -316,7 +316,7 @@ async def test_reauth_panel_disappeared(hass): }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "reauth_panel_disappeared"} @@ -358,7 +358,7 @@ async def test_reauth_invalid_pin(hass): }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -400,5 +400,5 @@ async def test_reauth_bad_login(hass): }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index d12b9a580c7..7585c2699cd 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user_flow_cannot_create_multiple_instances(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -37,7 +37,7 @@ async def test_user_flow_with_detected_dongle(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "detect" devices = result["data_schema"].schema.get("device").container assert FAKE_DONGLE_PATH in devices @@ -51,7 +51,7 @@ async def test_user_flow_with_no_detected_dongle(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "manual" @@ -64,7 +64,7 @@ async def test_detection_flow_with_valid_path(hass): DOMAIN, context={"source": "detect"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -82,7 +82,7 @@ async def test_detection_flow_with_custom_path(hass): data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "manual" @@ -100,7 +100,7 @@ async def test_detection_flow_with_invalid_path(hass): data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "detect" assert CONF_DEVICE in result["errors"] @@ -114,7 +114,7 @@ async def test_manual_flow_with_valid_path(hass): DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -130,7 +130,7 @@ async def test_manual_flow_with_invalid_path(hass): DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "manual" assert CONF_DEVICE in result["errors"] @@ -146,7 +146,7 @@ async def test_import_flow_with_valid_path(hass): data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == DATA_TO_IMPORT[CONF_DEVICE] @@ -164,5 +164,5 @@ async def test_import_flow_with_invalid_path(hass): data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_dongle_path" diff --git a/tests/components/enocean/test_switch.py b/tests/components/enocean/test_switch.py new file mode 100644 index 00000000000..a7aafa6fc73 --- /dev/null +++ b/tests/components/enocean/test_switch.py @@ -0,0 +1,73 @@ +"""Tests for the EnOcean switch platform.""" + +from enocean.utils import combine_hex + +from homeassistant.components.enocean import DOMAIN as ENOCEAN_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, assert_setup_component + +SWITCH_CONFIG = { + "switch": [ + { + "platform": ENOCEAN_DOMAIN, + "id": [0xDE, 0xAD, 0xBE, 0xEF], + "channel": 1, + "name": "room0", + }, + ] +} + + +async def test_unique_id_migration(hass: HomeAssistant) -> None: + """Test EnOcean switch ID migration.""" + + entity_name = SWITCH_CONFIG["switch"][0]["name"] + switch_entity_id = f"{SWITCH_DOMAIN}.{entity_name}" + dev_id = SWITCH_CONFIG["switch"][0]["id"] + channel = SWITCH_CONFIG["switch"][0]["channel"] + + ent_reg = er.async_get(hass) + + old_unique_id = f"{combine_hex(dev_id)}" + + entry = MockConfigEntry(domain=ENOCEAN_DOMAIN, data={"device": "/dev/null"}) + + entry.add_to_hass(hass) + + # Add a switch with an old unique_id to the entity registry + entity_entry = ent_reg.async_get_or_create( + SWITCH_DOMAIN, + ENOCEAN_DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=entry, + original_name=entity_name, + ) + + assert entity_entry.entity_id == switch_entity_id + assert entity_entry.unique_id == old_unique_id + + # Now add the sensor to check, whether the old unique_id is migrated + + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + SWITCH_CONFIG, + ) + + await hass.async_block_till_done() + + # Check that new entry has a new unique_id + entity_entry = ent_reg.async_get(switch_entity_id) + new_unique_id = f"{combine_hex(dev_id)}-{channel}" + + assert entity_entry.unique_id == new_unique_id + assert ( + ent_reg.async_get_entity_id(SWITCH_DOMAIN, ENOCEAN_DOMAIN, old_unique_id) + is None + ) diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index de3ff516eae..9d484d984a8 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -64,7 +64,7 @@ async def test_create_entry(hass): flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE @@ -89,7 +89,7 @@ async def test_create_same_entry_twice(hass): flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -135,6 +135,6 @@ async def test_lat_lon_not_specified(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=fake_config ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 1d2cff051ae..43e2f916082 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -15,11 +15,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -66,7 +62,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): data=None, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) @@ -77,7 +73,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 80, @@ -109,7 +105,7 @@ async def test_user_resolve_error(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "resolve_error"} @@ -128,7 +124,7 @@ async def test_user_connection_error(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} @@ -147,14 +143,14 @@ async def test_user_with_password(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password1"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -174,7 +170,7 @@ async def test_user_invalid_password(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = InvalidAuthAPIError @@ -183,7 +179,7 @@ async def test_user_invalid_password(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_PASSWORD: "invalid"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "invalid_auth"} @@ -198,7 +194,7 @@ async def test_login_connection_error(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = APIConnectionError @@ -207,7 +203,7 @@ async def test_login_connection_error(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_PASSWORD: "valid"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "connection_error"} @@ -233,7 +229,7 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): flow["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test8266" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -264,7 +260,7 @@ async def test_discovery_already_configured_hostname(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -292,7 +288,7 @@ async def test_discovery_already_configured_ip(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -324,7 +320,7 @@ async def test_discovery_already_configured_name(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -348,13 +344,13 @@ async def test_discovery_duplicate_data(hass, mock_client): result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -380,7 +376,7 @@ async def test_discovery_updates_unique_id(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -396,7 +392,7 @@ async def test_user_requires_psk(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {} @@ -416,7 +412,7 @@ async def test_encryption_key_valid_psk(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) @@ -424,7 +420,7 @@ async def test_encryption_key_valid_psk(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -445,7 +441,7 @@ async def test_encryption_key_invalid_psk(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError @@ -453,7 +449,7 @@ async def test_encryption_key_invalid_psk(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {"base": "invalid_psk"} assert mock_client.noise_psk == INVALID_NOISE_PSK @@ -475,7 +471,7 @@ async def test_reauth_initiation(hass, mock_client, mock_zeroconf): "unique_id": entry.unique_id, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -501,7 +497,7 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -528,7 +524,7 @@ async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" @@ -556,7 +552,7 @@ async def test_discovery_dhcp_updates_host(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -585,7 +581,7 @@ async def test_discovery_dhcp_no_changes(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" diff --git a/tests/components/evil_genius_labs/test_config_flow.py b/tests/components/evil_genius_labs/test_config_flow.py index e3354f4b9cc..19434c77d3c 100644 --- a/tests/components/evil_genius_labs/test_config_flow.py +++ b/tests/components/evil_genius_labs/test_config_flow.py @@ -7,7 +7,7 @@ import aiohttp from homeassistant import config_entries from homeassistant.components.evil_genius_labs.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form( @@ -17,7 +17,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -41,7 +41,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Fibonacci256-23D4" assert result2["data"] == { "host": "1.1.1.1", @@ -66,7 +66,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, caplog) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert "Unable to connect" in caplog.text @@ -88,7 +88,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "timeout"} @@ -109,5 +109,5 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index 4ba3b5911f6..c31a43c1949 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -29,11 +29,7 @@ from homeassistant.const import ( CONF_URL, CONF_USERNAME, ) -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( DISCOVERY_INFO, @@ -50,7 +46,7 @@ async def test_user_form(hass, ezviz_config_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -61,7 +57,7 @@ async def test_user_form(hass, ezviz_config_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**USER_INPUT} @@ -70,7 +66,7 @@ async def test_user_form(hass, ezviz_config_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured_account" @@ -85,7 +81,7 @@ async def test_user_custom_url(hass, ezviz_config_flow): {CONF_USERNAME: "test-user", CONF_PASSWORD: "test-pass", CONF_URL: "customize"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -95,7 +91,7 @@ async def test_user_custom_url(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_PASSWORD: "test-pass", CONF_TYPE: ATTR_TYPE_CLOUD, @@ -112,7 +108,7 @@ async def test_step_discovery_abort_if_cloud_account_missing(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -125,7 +121,7 @@ async def test_step_discovery_abort_if_cloud_account_missing(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ezviz_cloud_account_missing" @@ -139,7 +135,7 @@ async def test_async_step_integration_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -153,7 +149,7 @@ async def test_async_step_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_PASSWORD: "test-pass", CONF_TYPE: ATTR_TYPE_CAMERA, @@ -172,7 +168,7 @@ async def test_options_flow(hass): assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -182,7 +178,7 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "/H.264" assert result["data"][CONF_TIMEOUT] == 25 @@ -202,7 +198,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -213,7 +209,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -224,7 +220,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -235,7 +231,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -252,7 +248,7 @@ async def test_discover_exception_step1( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -267,7 +263,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -281,7 +277,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -295,7 +291,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -309,7 +305,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -327,7 +323,7 @@ async def test_discover_exception_step3( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -342,7 +338,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -356,7 +352,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -370,7 +366,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -390,7 +386,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -399,7 +395,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_auth"} @@ -410,7 +406,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_host"} @@ -421,7 +417,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "cannot_connect"} @@ -432,5 +428,5 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index 2ab4aaf6dd6..8b869d4830e 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -56,7 +56,7 @@ async def test_duplicate_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index f056f484a58..14f28257588 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -14,17 +14,23 @@ TEST_NAME = "my_fibaro_home_center" TEST_URL = "http://192.168.1.1/api/" TEST_USERNAME = "user" TEST_PASSWORD = "password" +TEST_VERSION = "4.360" @pytest.fixture(name="fibaro_client", autouse=True) def fibaro_client_fixture(): """Mock common methods and attributes of fibaro client.""" info_mock = Mock() - info_mock.get.return_value = Mock(serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME) + info_mock.get.return_value = Mock( + serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME, softVersion=TEST_VERSION + ) array_mock = Mock() array_mock.list.return_value = [] + client_mock = Mock() + client_mock.base_url.return_value = TEST_URL + with patch("fiblary3.client.v4.client.Client.__init__", return_value=None,), patch( "fiblary3.client.v4.client.Client.info", info_mock, @@ -37,6 +43,10 @@ def fibaro_client_fixture(): "fiblary3.client.v4.client.Client.scenes", array_mock, create=True, + ), patch( + "fiblary3.client.v4.client.Client.client", + client_mock, + create=True, ): yield diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index 16f0ab38dc1..ed9d4004b1a 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -5,11 +5,7 @@ from homeassistant.components.filesize.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import TEST_DIR, TEST_FILE, TEST_FILE_NAME, async_create_file @@ -24,7 +20,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={CONF_FILE_PATH: TEST_FILE}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == TEST_FILE_NAME assert result2.get("data") == {CONF_FILE_PATH: TEST_FILE} @@ -51,7 +47,7 @@ async def test_unique_path( DOMAIN, context={"source": SOURCE_USER}, data={CONF_FILE_PATH: TEST_FILE} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -64,7 +60,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER result2 = await hass.config_entries.flow.async_configure( @@ -103,7 +99,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_FILE_NAME assert result2["data"] == { CONF_FILE_PATH: TEST_FILE, diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index 0553574ae77..35467ffc449 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -42,7 +42,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_abort_if_already_setup(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -91,7 +91,7 @@ async def test_step_user(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_CONF[CONF_USERNAME] assert result["data"] == { "auth_implementation": "fireservicerota", @@ -131,7 +131,7 @@ async def test_reauth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.fireservicerota.config_flow.FireServiceRota" @@ -147,5 +147,5 @@ async def test_reauth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/fivem/test_config_flow.py b/tests/components/fivem/test_config_flow.py index a1a1e8f2a37..121b416a110 100644 --- a/tests/components/fivem/test_config_flow.py +++ b/tests/components/fivem/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.fivem.config_flow import DEFAULT_PORT from homeassistant.components.fivem.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType USER_INPUT = { CONF_HOST: "fivem.dummyserver.com", @@ -54,7 +54,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -63,7 +63,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -79,7 +79,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == USER_INPUT[CONF_HOST] assert result2["data"] == USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -101,7 +101,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -121,7 +121,7 @@ async def test_form_invalid(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -141,5 +141,5 @@ async def test_form_invalid_game_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_game_name"} diff --git a/tests/components/fjaraskupan/__init__.py b/tests/components/fjaraskupan/__init__.py index 26a5ecd6605..35c69f98d65 100644 --- a/tests/components/fjaraskupan/__init__.py +++ b/tests/components/fjaraskupan/__init__.py @@ -1 +1,11 @@ """Tests for the Fjäråskupan integration.""" + + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import SOURCE_LOCAL, BluetoothServiceInfoBleak + +COOKER_SERVICE_INFO = BluetoothServiceInfoBleak.from_advertisement( + BLEDevice("1.1.1.1", "COOKERHOOD_FJAR"), AdvertisementData(), source=SOURCE_LOCAL +) diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index d60abcdb9ad..46ff5ae167a 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -1,41 +1,9 @@ """Standard fixtures for the Fjäråskupan integration.""" from __future__ import annotations -from unittest.mock import patch - -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData, BaseBleakScanner -from pytest import fixture +import pytest -@fixture(name="scanner", autouse=True) -def fixture_scanner(hass): - """Fixture for scanner.""" - - devices = [BLEDevice("1.1.1.1", "COOKERHOOD_FJAR")] - - class MockScanner(BaseBleakScanner): - """Mock Scanner.""" - - async def start(self): - """Start scanning for devices.""" - for device in devices: - self._callback(device, AdvertisementData()) - - async def stop(self): - """Stop scanning for devices.""" - - @property - def discovered_devices(self) -> list[BLEDevice]: - """Return discovered devices.""" - return devices - - def set_scanning_filter(self, **kwargs): - """Set the scanning filter.""" - - with patch( - "homeassistant.components.fjaraskupan.config_flow.BleakScanner", new=MockScanner - ), patch( - "homeassistant.components.fjaraskupan.config_flow.CONST_WAIT_TIME", new=0.01 - ): - yield devices +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index 22808382c49..bef53e18073 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -3,17 +3,14 @@ from __future__ import annotations from unittest.mock import patch -from bleak.backends.device import BLEDevice from pytest import fixture from homeassistant import config_entries from homeassistant.components.fjaraskupan.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType + +from . import COOKER_SERVICE_INFO @fixture(name="mock_setup_entry", autouse=True) @@ -28,31 +25,38 @@ async def fixture_mock_setup_entry(hass): async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[COOKER_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Fjäråskupan" - assert result["data"] == {} + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Fjäråskupan" + assert result["data"] == {} - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 -async def test_scan_no_devices(hass: HomeAssistant, scanner: list[BLEDevice]) -> None: +async def test_scan_no_devices(hass: HomeAssistant) -> None: """Test we get the form.""" - scanner.clear() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "no_devices_found" + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index be4f240efa3..ad6f32c5611 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -43,7 +43,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Flick Electric: test-username" assert result2["data"] == CONF assert len(mock_setup_entry.mock_calls) == 1 @@ -65,7 +65,7 @@ async def test_form_duplicate_login(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -77,7 +77,7 @@ async def test_form_invalid_auth(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -89,7 +89,7 @@ async def test_form_cannot_connect(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -101,5 +101,5 @@ async def test_form_generic_exception(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/flipr/test_config_flow.py b/tests/components/flipr/test_config_flow.py index 00c8d7e2401..484cba2f4f4 100644 --- a/tests/components/flipr/test_config_flow.py +++ b/tests/components/flipr/test_config_flow.py @@ -25,7 +25,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER @@ -67,7 +67,7 @@ async def test_nominal_case(hass, mock_setup): assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "flipid" assert result["data"] == { CONF_EMAIL: "dummylogin", @@ -91,7 +91,7 @@ async def test_multiple_flip_id(hass, mock_setup): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "flipr_id" result = await hass.config_entries.flow.async_configure( @@ -101,7 +101,7 @@ async def test_multiple_flip_id(hass, mock_setup): assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "FLIP2" assert result["data"] == { CONF_EMAIL: "dummylogin", diff --git a/tests/components/flo/test_binary_sensor.py b/tests/components/flo/test_binary_sensor.py index b6a8abf727c..36901584cf6 100644 --- a/tests/components/flo/test_binary_sensor.py +++ b/tests/components/flo/test_binary_sensor.py @@ -22,12 +22,17 @@ async def test_binary_sensors(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 - valve_state = hass.states.get("binary_sensor.pending_system_alerts") + valve_state = hass.states.get( + "binary_sensor.smart_water_shutoff_pending_system_alerts" + ) assert valve_state.state == STATE_ON assert valve_state.attributes.get("info") == 0 assert valve_state.attributes.get("warning") == 2 assert valve_state.attributes.get("critical") == 0 - assert valve_state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending System Alerts" + assert ( + valve_state.attributes.get(ATTR_FRIENDLY_NAME) + == "Smart water shutoff Pending system alerts" + ) - detector_state = hass.states.get("binary_sensor.water_detected") + detector_state = hass.states.get("binary_sensor.kitchen_sink_water_detected") assert detector_state.state == STATE_OFF diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index 0a7cf16ecf5..b5439241d33 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -18,48 +18,63 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 # we should have 5 entities for the valve - assert hass.states.get("sensor.current_system_mode").state == "home" - - assert hass.states.get("sensor.today_s_water_usage").state == "3.7" + print(hass.states) assert ( - hass.states.get("sensor.today_s_water_usage").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_current_system_mode").state + == "home" + ) + + assert ( + hass.states.get("sensor.smart_water_shutoff_today_s_water_usage").state == "3.7" + ) + assert ( + hass.states.get("sensor.smart_water_shutoff_today_s_water_usage").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.TOTAL_INCREASING ) - assert hass.states.get("sensor.water_flow_rate").state == "0" + assert hass.states.get("sensor.smart_water_shutoff_water_flow_rate").state == "0" assert ( - hass.states.get("sensor.water_flow_rate").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_water_flow_rate").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.water_pressure").state == "54.2" + assert hass.states.get("sensor.smart_water_shutoff_water_pressure").state == "54.2" assert ( - hass.states.get("sensor.water_pressure").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_water_pressure").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.water_temperature").state == "21" + assert hass.states.get("sensor.smart_water_shutoff_water_temperature").state == "21" assert ( - hass.states.get("sensor.water_temperature").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_water_temperature").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.MEASUREMENT ) # and 3 entities for the detector - assert hass.states.get("sensor.temperature").state == "16" + print(hass.states) + assert hass.states.get("sensor.kitchen_sink_temperature").state == "16" assert ( - hass.states.get("sensor.temperature").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.kitchen_sink_temperature").attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.humidity").state == "43" + assert hass.states.get("sensor.kitchen_sink_humidity").state == "43" assert ( - hass.states.get("sensor.humidity").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.kitchen_sink_humidity").attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.battery").state == "100" + assert hass.states.get("sensor.kitchen_sink_battery").state == "100" assert ( - hass.states.get("sensor.battery").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.kitchen_sink_battery").attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ) @@ -82,7 +97,7 @@ async def test_manual_update_entity( await hass.services.async_call( "homeassistant", "update_entity", - {ATTR_ENTITY_ID: ["sensor.current_system_mode"]}, + {ATTR_ENTITY_ID: ["sensor.smart_water_shutoff_current_system_mode"]}, blocking=True, ) assert aioclient_mock.call_count == call_count + 3 diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py index 720d0596b22..c7e2e070745 100644 --- a/tests/components/flo/test_services.py +++ b/tests/components/flo/test_services.py @@ -17,7 +17,7 @@ from homeassistant.setup import async_setup_component from .common import TEST_PASSWORD, TEST_USER_ID -SWITCH_ENTITY_ID = "switch.shutoff_valve" +SWITCH_ENTITY_ID = "switch.smart_water_shutoff_shutoff_valve" async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mock): diff --git a/tests/components/flo/test_switch.py b/tests/components/flo/test_switch.py index cc6ab7e3a7e..5f97489fcde 100644 --- a/tests/components/flo/test_switch.py +++ b/tests/components/flo/test_switch.py @@ -17,7 +17,7 @@ async def test_valve_switches(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 - entity_id = "switch.shutoff_valve" + entity_id = "switch.smart_water_shutoff_shutoff_valve" assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py index 8c22d5fc915..c0fe58ea811 100644 --- a/tests/components/flunearyou/test_config_flow.py +++ b/tests/components/flunearyou/test_config_flow.py @@ -14,7 +14,7 @@ async def test_duplicate_error(hass, config, config_entry, setup_flunearyou): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -35,7 +35,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -44,7 +44,7 @@ async def test_step_user(hass, config, setup_flunearyou): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "51.528308, -0.3817765" assert result["data"] == { CONF_LATITUDE: 51.528308, diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 9750600518e..8abdb8e955b 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -24,7 +24,7 @@ from homeassistant.components.flux_led.const import ( ) from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_TITLE, @@ -386,7 +386,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=FLUX_DISCOVERY, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(): @@ -396,7 +396,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_wifibulb(): @@ -410,7 +410,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -425,7 +425,7 @@ async def test_discovered_by_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(), patch( @@ -462,7 +462,7 @@ async def test_discovered_by_dhcp_udp_responds(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(), patch( @@ -499,7 +499,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(no_device=True), _patch_wifibulb(), patch( @@ -529,7 +529,7 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(device=FLUX_DISCOVERY_PARTIAL), _patch_wifibulb(), patch( @@ -560,7 +560,7 @@ async def test_discovered_by_dhcp_no_udp_response_or_tcp_response(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -584,7 +584,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -605,7 +605,7 @@ async def test_mac_address_off_by_one_updated_via_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -624,7 +624,7 @@ async def test_mac_address_off_by_one_not_updated_from_dhcp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_ONE_OFF @@ -652,7 +652,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_DIFFERENT diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index f0611d1678d..2380e65aabb 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components.forecast_solar.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +23,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -42,7 +42,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_LATITUDE: 52.42, @@ -70,7 +70,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -86,7 +86,7 @@ async def test_options_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_API_KEY: "solarPOWER!", CONF_DECLINATION: 21, diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index a05acb5bc16..893730c722e 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensors( assert state.state == "100.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - Today" + == "Solar production forecast Estimated energy production - today" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -56,7 +56,7 @@ async def test_sensors( assert state.state == "200.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - Tomorrow" + == "Solar production forecast Estimated energy production - tomorrow" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -69,7 +69,10 @@ async def test_sensors( assert state assert entry.unique_id == f"{entry_id}_power_highest_peak_time_today" assert state.state == "2021-06-27T20:00:00+00:00" # Timestamp sensor is UTC - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Today" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Solar production forecast Highest power peak time - today" + ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -82,7 +85,8 @@ async def test_sensors( assert entry.unique_id == f"{entry_id}_power_highest_peak_time_tomorrow" assert state.state == "2021-06-27T21:00:00+00:00" # Timestamp sensor is UTC assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Tomorrow" + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Solar production forecast Highest power peak time - tomorrow" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP @@ -96,7 +100,8 @@ async def test_sensors( assert entry.unique_id == f"{entry_id}_power_production_now" assert state.state == "300000" assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Power Production - Now" + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Solar production forecast Estimated power production - now" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -111,7 +116,7 @@ async def test_sensors( assert state.state == "800.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - This Hour" + == "Solar production forecast Estimated energy production - this hour" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -126,7 +131,7 @@ async def test_sensors( assert state.state == "900.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - Next Hour" + == "Solar production forecast Estimated energy production - next hour" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -138,7 +143,7 @@ async def test_sensors( assert device_entry assert device_entry.identifiers == {(DOMAIN, f"{entry_id}")} assert device_entry.manufacturer == "Forecast.Solar" - assert device_entry.name == "Solar Production Forecast" + assert device_entry.name == "Solar production forecast" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "public" assert not device_entry.sw_version @@ -172,17 +177,17 @@ async def test_disabled_by_default( [ ( "power_production_next_12hours", - "Estimated Power Production - Next 12 Hours", + "Estimated power production - next 12 hours", "600000", ), ( "power_production_next_24hours", - "Estimated Power Production - Next 24 Hours", + "Estimated power production - next 24 hours", "700000", ), ( "power_production_next_hour", - "Estimated Power Production - Next Hour", + "Estimated power production - next hour", "400000", ), ], @@ -219,7 +224,9 @@ async def test_enabling_disable_by_default( assert state assert entry.unique_id == f"{entry_id}_{key}" assert state.state == value - assert state.attributes.get(ATTR_FRIENDLY_NAME) == name + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == f"Solar production forecast {name}" + ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index fd4d82b177c..e810b0eb957 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -58,7 +58,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -78,7 +78,7 @@ async def test_config_flow(hass, config_entry): DOMAIN, context={"source": SOURCE_USER}, data=config_data ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "My Music on myhost" assert result["data"][CONF_HOST] == config_data[CONF_HOST] assert result["data"][CONF_PORT] == config_data[CONF_PORT] @@ -91,7 +91,7 @@ async def test_config_flow(hass, config_entry): data=config_entry.data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_zeroconf_updates_title(hass, config_entry): @@ -112,7 +112,7 @@ async def test_zeroconf_updates_title(hass, config_entry): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert config_entry.title == "zeroconf_test" assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -128,7 +128,7 @@ async def test_config_flow_no_websocket(hass, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config_entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_config_flow_zeroconf_invalid(hass): @@ -146,7 +146,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with forked-daapd version < 27 discovery_info = zeroconf.ZeroconfServiceInfo( @@ -161,7 +161,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with verbose mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -176,7 +176,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with svn mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -191,7 +191,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" @@ -213,7 +213,7 @@ async def test_config_flow_zeroconf_valid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_options_flow(hass, config_entry): @@ -229,7 +229,7 @@ async def test_options_flow(hass, config_entry): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -240,4 +240,4 @@ async def test_options_flow(hass, config_entry): CONF_MAX_PLAYLISTS: 8, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 63c30c16bab..e8bdb6900f2 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -80,7 +80,7 @@ async def test_user_valid(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -98,7 +98,7 @@ async def test_user_valid(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CAMERA_NAME assert result["data"] == VALID_CONFIG @@ -111,7 +111,7 @@ async def test_user_invalid_auth(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -129,7 +129,7 @@ async def test_user_invalid_auth(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -139,7 +139,7 @@ async def test_user_cannot_connect(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -157,7 +157,7 @@ async def test_user_cannot_connect(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -167,7 +167,7 @@ async def test_user_invalid_response(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -187,7 +187,7 @@ async def test_user_invalid_response(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_response"} @@ -203,7 +203,7 @@ async def test_user_already_configured(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -218,7 +218,7 @@ async def test_user_already_configured(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -228,7 +228,7 @@ async def test_user_unknown_exception(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -243,5 +243,5 @@ async def test_user_unknown_exception(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index cee1c28cebd..7922655b3b6 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -53,7 +53,7 @@ async def test_user(hass: HomeAssistant): context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -64,7 +64,7 @@ async def test_import(hass: HomeAssistant): context={"source": SOURCE_IMPORT}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -75,7 +75,7 @@ async def test_zeroconf(hass: HomeAssistant): context={"source": SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -94,7 +94,7 @@ async def test_link(hass: HomeAssistant, router: Mock): ) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == MOCK_HOST assert result["title"] == MOCK_HOST assert result["data"][CONF_HOST] == MOCK_HOST @@ -118,7 +118,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): context={"source": SOURCE_IMPORT}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same MOCK_HOST (flow) @@ -127,7 +127,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -144,7 +144,7 @@ async def test_on_link_failed(hass: HomeAssistant): side_effect=AuthorizationError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "register_failed"} with patch( @@ -152,7 +152,7 @@ async def test_on_link_failed(hass: HomeAssistant): side_effect=HttpRequestError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -160,5 +160,5 @@ async def test_on_link_failed(hass: HomeAssistant): side_effect=InvalidTokenError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py index 42dc0674d07..d1804437a59 100644 --- a/tests/components/freedompro/test_config_flow.py +++ b/tests/components/freedompro/test_config_flow.py @@ -19,7 +19,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -77,6 +77,6 @@ async def test_create_entry(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Freedompro" assert result["data"][CONF_API_KEY] == "ksdjfgslkjdfksjdfksjgfksjd" diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 619f06f5493..76f556d0743 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -18,11 +18,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .const import ( MOCK_FIRMWARE_INFO, @@ -62,13 +58,13 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -113,13 +109,13 @@ async def test_user_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "already_configured" @@ -130,7 +126,7 @@ async def test_exception_security(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -142,7 +138,7 @@ async def test_exception_security(hass: HomeAssistant, mock_get_source_ip): result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_AUTH_INVALID @@ -153,7 +149,7 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -165,7 +161,7 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip): result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_CANNOT_CONNECT @@ -176,7 +172,7 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -188,7 +184,7 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip): result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_UNKNOWN @@ -226,7 +222,7 @@ async def test_reauth_successful( data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -237,7 +233,7 @@ async def test_reauth_successful( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_setup_entry.called @@ -262,7 +258,7 @@ async def test_reauth_not_successful( data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -273,7 +269,7 @@ async def test_reauth_not_successful( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "cannot_connect" @@ -301,7 +297,7 @@ async def test_ssdp_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -328,7 +324,7 @@ async def test_ssdp_already_configured_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -355,7 +351,7 @@ async def test_ssdp_already_configured_host_uuid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -371,7 +367,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA) @@ -380,7 +376,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -408,7 +404,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -419,7 +415,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == MOCK_IPS["fritz.box"] assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -437,7 +433,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -448,7 +444,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" @@ -463,7 +459,7 @@ async def test_options_flow(hass: HomeAssistant, fc_class_mock, mock_get_source_ side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzBoxTools"): result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_init(mock_config.entry_id) @@ -473,5 +469,5 @@ async def test_options_flow(hass: HomeAssistant, fc_class_mock, mock_get_source_ CONF_CONSIDER_HOME: 37, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert mock_config.options[CONF_CONSIDER_HOME] == 37 diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 442b2f4d568..e82101b3977 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .const import CONF_FAKE_NAME, MOCK_CONFIG @@ -70,13 +66,13 @@ async def test_user(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -91,7 +87,7 @@ async def test_user_auth_failed(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -103,7 +99,7 @@ async def test_user_not_successful(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -112,13 +108,13 @@ async def test_user_already_configured(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -132,7 +128,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -143,7 +139,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_config.data[CONF_USERNAME] == "other_fake_user" assert mock_config.data[CONF_PASSWORD] == "other_fake_password" @@ -161,7 +157,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -172,7 +168,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -189,7 +185,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -200,16 +196,16 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @pytest.mark.parametrize( "test_data,expected_result", [ - (MOCK_SSDP_DATA["ip4_valid"], RESULT_TYPE_FORM), - (MOCK_SSDP_DATA["ip6_valid"], RESULT_TYPE_FORM), - (MOCK_SSDP_DATA["ip6_invalid"], RESULT_TYPE_ABORT), + (MOCK_SSDP_DATA["ip4_valid"], FlowResultType.FORM), + (MOCK_SSDP_DATA["ip6_valid"], FlowResultType.FORM), + (MOCK_SSDP_DATA["ip6_invalid"], FlowResultType.ABORT), ], ) async def test_ssdp( @@ -224,7 +220,7 @@ async def test_ssdp( ) assert result["type"] == expected_result - if expected_result == RESULT_TYPE_ABORT: + if expected_result == FlowResultType.ABORT: return assert result["step_id"] == "confirm" @@ -233,7 +229,7 @@ async def test_ssdp( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == CONF_FAKE_NAME assert result["data"][CONF_HOST] == urlparse(test_data.ssdp_location).hostname assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -249,14 +245,14 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_NAME ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -271,7 +267,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -279,7 +275,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock): result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"]["base"] == "invalid_auth" @@ -291,14 +287,14 @@ async def test_ssdp_not_successful(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -309,14 +305,14 @@ async def test_ssdp_not_supported(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -325,13 +321,13 @@ async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistant, fritz: Mo result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -340,7 +336,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA["ip4_valid"]) @@ -349,7 +345,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -358,12 +354,12 @@ async def test_ssdp_already_configured(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" assert result["result"].unique_id == "only-a-test" diff --git a/tests/components/fritzbox_callmonitor/test_config_flow.py b/tests/components/fritzbox_callmonitor/test_config_flow.py index 51884abe96f..94d5bdc8eeb 100644 --- a/tests/components/fritzbox_callmonitor/test_config_flow.py +++ b/tests/components/fritzbox_callmonitor/test_config_flow.py @@ -22,11 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, patch @@ -74,7 +70,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -104,7 +100,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_1 assert result["data"] == MOCK_CONFIG_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -116,7 +112,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -140,7 +136,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "phonebook" assert result["errors"] == {} @@ -156,7 +152,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: {CONF_PHONEBOOK: MOCK_PHONEBOOK_NAME_2}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_2 assert result["data"] == { CONF_HOST: MOCK_HOST, @@ -184,7 +180,7 @@ async def test_setup_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == ConnectResult.NO_DEVIES_FOUND @@ -203,7 +199,7 @@ async def test_setup_insufficient_permissions(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == ConnectResult.INSUFFICIENT_PERMISSIONS @@ -222,7 +218,7 @@ async def test_setup_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.INVALID_AUTH} @@ -244,14 +240,14 @@ async def test_options_flow_correct_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_PREFIXES: "+49, 491234"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: ["+49", "491234"]} @@ -273,14 +269,14 @@ async def test_options_flow_incorrect_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_PREFIXES: ""} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.MALFORMED_PREFIXES} @@ -302,12 +298,12 @@ async def test_options_flow_no_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: None} diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index 256d64d4cbe..69f1dffa64b 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fronius.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import mock_responses @@ -51,7 +47,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -69,7 +65,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Datalogger at 10.9.8.1" assert result2["data"] == { "host": "10.9.8.1", @@ -83,7 +79,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -104,7 +100,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Inverter at 10.9.1.1" assert result2["data"] == { "host": "10.9.1.1", @@ -133,7 +129,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -157,7 +153,7 @@ async def test_form_no_device(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -178,7 +174,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -206,7 +202,7 @@ async def test_form_already_existing(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -246,7 +242,7 @@ async def test_form_updates_host(hass, aioclient_mock): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" mock_unload_entry.assert_called_with(hass, entry) @@ -269,13 +265,13 @@ async def test_dhcp(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm_discovery" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"SolarNet Datalogger at {MOCK_DHCP_DATA.ip}" assert result["data"] == { "host": MOCK_DHCP_DATA.ip, @@ -298,7 +294,7 @@ async def test_dhcp_already_configured(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -313,5 +309,5 @@ async def test_dhcp_invalid(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_host" diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index 0f3e8f28a56..3ed1e37505c 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -30,11 +30,11 @@ async def test_symo_inverter(hass, aioclient_mock): hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 10828) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44186900) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 25507686) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 16) + assert_state("sensor.symo_20_current_dc", 0) + assert_state("sensor.symo_20_energy_day", 10828) + assert_state("sensor.symo_20_energy_total", 44186900) + assert_state("sensor.symo_20_energy_year", 25507686) + assert_state("sensor.symo_20_voltage_dc", 16) # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) @@ -48,15 +48,15 @@ async def test_symo_inverter(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # 4 additional AC entities - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 2.19) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 1113) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44188000) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 25508798) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 518) - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 5.19) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.94) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 1190) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.90) + assert_state("sensor.symo_20_current_dc", 2.19) + assert_state("sensor.symo_20_energy_day", 1113) + assert_state("sensor.symo_20_energy_total", 44188000) + assert_state("sensor.symo_20_energy_year", 25508798) + assert_state("sensor.symo_20_voltage_dc", 518) + assert_state("sensor.symo_20_current_ac", 5.19) + assert_state("sensor.symo_20_frequency_ac", 49.94) + assert_state("sensor.symo_20_power_ac", 1190) + assert_state("sensor.symo_20_voltage_ac", 227.90) # Third test at nighttime - additional AC entities aren't changed mock_responses(aioclient_mock, night=True) @@ -64,10 +64,10 @@ async def test_symo_inverter(hass, aioclient_mock): hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 5.19) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.94) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 1190) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.90) + assert_state("sensor.symo_20_current_ac", 5.19) + assert_state("sensor.symo_20_frequency_ac", 49.94) + assert_state("sensor.symo_20_power_ac", 1190) + assert_state("sensor.symo_20_voltage_ac", 227.90) async def test_symo_logger(hass, aioclient_mock): @@ -82,18 +82,9 @@ async def test_symo_logger(hass, aioclient_mock): await setup_fronius_integration(hass) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24 # states are rounded to 4 decimals - assert_state( - "sensor.cash_factor_fronius_logger_info_0_http_fronius", - 0.078, - ) - assert_state( - "sensor.co2_factor_fronius_logger_info_0_http_fronius", - 0.53, - ) - assert_state( - "sensor.delivery_factor_fronius_logger_info_0_http_fronius", - 0.15, - ) + assert_state("sensor.solarnet_grid_export_tariff", 0.078) + assert_state("sensor.solarnet_co2_factor", 0.53) + assert_state("sensor.solarnet_grid_import_tariff", 0.15) async def test_symo_meter(hass, aioclient_mock): @@ -113,48 +104,38 @@ async def test_symo_meter(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # states are rounded to 4 decimals - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 7.755) - assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 6.68) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 10.102) - assert_state( - "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 59960790 - ) - assert_state( - "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 723160 - ) - assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 35623065) - assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 15303334) - assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 15303334) - assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 35623065) - assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 50) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 1772.793) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 1527.048) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 2333.562) - assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 5592.57) - assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", -0.99) - assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", -0.99) - assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.99) - assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 1) - assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", 51.48) - assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", 115.63) - assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -164.24) - assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", 2.87) - assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 1765.55) - assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 1515.8) - assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 2311.22) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", 5592.57) - assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 228.6) - assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 228.6) - assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 231) - assert_state( - "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 395.9 - ) - assert_state( - "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 398 - ) - assert_state( - "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 398 - ) + assert_state("sensor.smart_meter_63a_current_ac_phase_1", 7.755) + assert_state("sensor.smart_meter_63a_current_ac_phase_2", 6.68) + assert_state("sensor.smart_meter_63a_current_ac_phase_3", 10.102) + assert_state("sensor.smart_meter_63a_energy_reactive_ac_consumed", 59960790) + assert_state("sensor.smart_meter_63a_energy_reactive_ac_produced", 723160) + assert_state("sensor.smart_meter_63a_energy_real_ac_minus", 35623065) + assert_state("sensor.smart_meter_63a_energy_real_ac_plus", 15303334) + assert_state("sensor.smart_meter_63a_energy_real_consumed", 15303334) + assert_state("sensor.smart_meter_63a_energy_real_produced", 35623065) + assert_state("sensor.smart_meter_63a_frequency_phase_average", 50) + assert_state("sensor.smart_meter_63a_power_apparent_phase_1", 1772.793) + assert_state("sensor.smart_meter_63a_power_apparent_phase_2", 1527.048) + assert_state("sensor.smart_meter_63a_power_apparent_phase_3", 2333.562) + assert_state("sensor.smart_meter_63a_power_apparent", 5592.57) + assert_state("sensor.smart_meter_63a_power_factor_phase_1", -0.99) + assert_state("sensor.smart_meter_63a_power_factor_phase_2", -0.99) + assert_state("sensor.smart_meter_63a_power_factor_phase_3", 0.99) + assert_state("sensor.smart_meter_63a_power_factor", 1) + assert_state("sensor.smart_meter_63a_power_reactive_phase_1", 51.48) + assert_state("sensor.smart_meter_63a_power_reactive_phase_2", 115.63) + assert_state("sensor.smart_meter_63a_power_reactive_phase_3", -164.24) + assert_state("sensor.smart_meter_63a_power_reactive", 2.87) + assert_state("sensor.smart_meter_63a_power_real_phase_1", 1765.55) + assert_state("sensor.smart_meter_63a_power_real_phase_2", 1515.8) + assert_state("sensor.smart_meter_63a_power_real_phase_3", 2311.22) + assert_state("sensor.smart_meter_63a_power_real", 5592.57) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_1", 228.6) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_2", 228.6) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_3", 231) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_1_2", 395.9) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_2_3", 398) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_3_1", 398) async def test_symo_power_flow(hass, aioclient_mock): @@ -175,30 +156,12 @@ async def test_symo_power_flow(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # states are rounded to 4 decimals - assert_state( - "sensor.energy_day_fronius_power_flow_0_http_fronius", - 10828, - ) - assert_state( - "sensor.energy_total_fronius_power_flow_0_http_fronius", - 44186900, - ) - assert_state( - "sensor.energy_year_fronius_power_flow_0_http_fronius", - 25507686, - ) - assert_state( - "sensor.power_grid_fronius_power_flow_0_http_fronius", - 975.31, - ) - assert_state( - "sensor.power_load_fronius_power_flow_0_http_fronius", - -975.31, - ) - assert_state( - "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", - 0, - ) + assert_state("sensor.solarnet_energy_day", 10828) + assert_state("sensor.solarnet_energy_total", 44186900) + assert_state("sensor.solarnet_energy_year", 25507686) + assert_state("sensor.solarnet_power_grid", 975.31) + assert_state("sensor.solarnet_power_load", -975.31) + assert_state("sensor.solarnet_relative_autonomy", 0) # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) @@ -208,38 +171,14 @@ async def test_symo_power_flow(hass, aioclient_mock): await hass.async_block_till_done() # 54 because power_flow `rel_SelfConsumption` and `P_PV` is not `null` anymore assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 - assert_state( - "sensor.energy_day_fronius_power_flow_0_http_fronius", - 1101.7001, - ) - assert_state( - "sensor.energy_total_fronius_power_flow_0_http_fronius", - 44188000, - ) - assert_state( - "sensor.energy_year_fronius_power_flow_0_http_fronius", - 25508788, - ) - assert_state( - "sensor.power_grid_fronius_power_flow_0_http_fronius", - 1703.74, - ) - assert_state( - "sensor.power_load_fronius_power_flow_0_http_fronius", - -2814.74, - ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", - 1111, - ) - assert_state( - "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", - 39.4708, - ) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", - 100, - ) + assert_state("sensor.solarnet_energy_day", 1101.7001) + assert_state("sensor.solarnet_energy_total", 44188000) + assert_state("sensor.solarnet_energy_year", 25508788) + assert_state("sensor.solarnet_power_grid", 1703.74) + assert_state("sensor.solarnet_power_load", -2814.74) + assert_state("sensor.solarnet_power_photovoltaics", 1111) + assert_state("sensor.solarnet_relative_autonomy", 39.4708) + assert_state("sensor.solarnet_relative_self_consumption", 100) async def test_gen24(hass, aioclient_mock): @@ -259,74 +198,60 @@ async def test_gen24(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # inverter 1 - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 0.1589) - assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.0754) - assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.0783) - assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 403.4312) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 37.3204) - assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 411.3811) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 1530193.42) - assert_state("sensor.inverter_state_fronius_inverter_1_http_fronius", "Running") - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 234.9168) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.9917) + assert_state("sensor.inverter_name_current_ac", 0.1589) + assert_state("sensor.inverter_name_current_dc_2", 0.0754) + assert_state("sensor.inverter_name_status_code", 7) + assert_state("sensor.inverter_name_current_dc", 0.0783) + assert_state("sensor.inverter_name_voltage_dc_2", 403.4312) + assert_state("sensor.inverter_name_power_ac", 37.3204) + assert_state("sensor.inverter_name_error_code", 0) + assert_state("sensor.inverter_name_voltage_dc", 411.3811) + assert_state("sensor.inverter_name_energy_total", 1530193.42) + assert_state("sensor.inverter_name_inverter_state", "Running") + assert_state("sensor.inverter_name_voltage_ac", 234.9168) + assert_state("sensor.inverter_name_frequency_ac", 49.9917) # meter - assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 3863340.0) - assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 2013105.0) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", 653.1) - assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 49.9) - assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 0.0) - assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 0.828) - assert_state( - "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 88221.0 - ) - assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 3863340.0) - assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 2.33) - assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 235.9) - assert_state( - "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 408.7 - ) - assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 294.9) - assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 2013105.0) - assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 236.1) - assert_state( - "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 1989125.0 - ) - assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 236.9) - assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", 0.441) - assert_state( - "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 409.6 - ) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 1.825) - assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.832) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 243.3) - assert_state( - "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 409.4 - ) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 323.4) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 301.2) - assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 106.8) - assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", 0.934) - assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 251.3) - assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", -218.6) - assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", -132.8) - assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -166.0) - assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 868.0) - assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", -517.4) - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 1.145) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_produced", 3863340.0) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_consumed", 2013105.0) + assert_state("sensor.smart_meter_ts_65a_3_power_real", 653.1) + assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9) + assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0) + assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.828) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_consumed", 88221.0) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_minus", 3863340.0) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_2", 2.33) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1", 235.9) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1_2", 408.7) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_2", 294.9) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_plus", 2013105.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2", 236.1) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_produced", 1989125.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3", 236.9) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_1", 0.441) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2_3", 409.6) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_3", 1.825) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_3", 0.832) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_1", 243.3) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3_1", 409.4) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_2", 323.4) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_3", 301.2) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_1", 106.8) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_2", 0.934) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_3", 251.3) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_1", -218.6) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_2", -132.8) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_3", -166.0) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent", 868.0) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive", -517.4) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_1", 1.145) # power_flow - assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 658.4) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100.0 - ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 62.9481 - ) - assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -695.6827) - assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "meter") - assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 5.3592) - assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 1530193.42) + assert_state("sensor.solarnet_power_grid", 658.4) + assert_state("sensor.solarnet_relative_self_consumption", 100.0) + assert_state("sensor.solarnet_power_photovoltaics", 62.9481) + assert_state("sensor.solarnet_power_load", -695.6827) + assert_state("sensor.solarnet_meter_mode", "meter") + assert_state("sensor.solarnet_relative_autonomy", 5.3592) + assert_state("sensor.solarnet_energy_total", 1530193.42) async def test_gen24_storage(hass, aioclient_mock): @@ -348,92 +273,74 @@ async def test_gen24_storage(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 64 # inverter 1 - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.3952) - assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 318.8103) - assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.3564) - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 1.1087) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 250.9093) - assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 7512794.0117) - assert_state("sensor.inverter_state_fronius_inverter_1_http_fronius", "Running") - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 419.1009) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.354) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.9816) + assert_state("sensor.gen24_storage_current_dc", 0.3952) + assert_state("sensor.gen24_storage_voltage_dc_2", 318.8103) + assert_state("sensor.gen24_storage_current_dc_2", 0.3564) + assert_state("sensor.gen24_storage_current_ac", 1.1087) + assert_state("sensor.gen24_storage_power_ac", 250.9093) + assert_state("sensor.gen24_storage_error_code", 0) + assert_state("sensor.gen24_storage_status_code", 7) + assert_state("sensor.gen24_storage_energy_total", 7512794.0117) + assert_state("sensor.gen24_storage_inverter_state", "Running") + assert_state("sensor.gen24_storage_voltage_dc", 419.1009) + assert_state("sensor.gen24_storage_voltage_ac", 227.354) + assert_state("sensor.gen24_storage_frequency_ac", 49.9816) # meter - assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 1705128.0) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", 487.7) - assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 0.698) - assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 1247204.0) - assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 49.9) - assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 0.0) - assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", -501.5) - assert_state( - "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 3266105.0 - ) - assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 19.6) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 0.645) - assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 1705128.0) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 383.9) - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 1.701) - assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 1.832) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 319.5) - assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 229.4) - assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 150.0) - assert_state( - "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 394.3 - ) - assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 225.6) - assert_state( - "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 5482.0 - ) - assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 1247204.0) - assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", 0.995) - assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.163) - assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", 0.389) - assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", -31.3) - assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -116.7) - assert_state( - "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 396.0 - ) - assert_state( - "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 393.0 - ) - assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", -353.4) - assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 317.9) - assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 228.3) - assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 821.9) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 118.4) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_produced", 1705128.0) + assert_state("sensor.smart_meter_ts_65a_3_power_real", 487.7) + assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.698) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_consumed", 1247204.0) + assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9) + assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive", -501.5) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_produced", 3266105.0) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_3", 19.6) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_3", 0.645) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_minus", 1705128.0) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_2", 383.9) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_1", 1.701) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_2", 1.832) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_1", 319.5) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1", 229.4) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_2", 150.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3_1", 394.3) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2", 225.6) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_consumed", 5482.0) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_plus", 1247204.0) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_1", 0.995) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_3", 0.163) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_2", 0.389) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_1", -31.3) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_3", -116.7) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1_2", 396.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2_3", 393.0) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_2", -353.4) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_1", 317.9) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3", 228.3) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent", 821.9) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_3", 118.4) # ohmpilot - assert_state( - "sensor.energy_real_ac_consumed_fronius_ohmpilot_0_http_fronius", 1233295.0 - ) - assert_state("sensor.power_real_ac_fronius_ohmpilot_0_http_fronius", 0.0) - assert_state("sensor.temperature_channel_1_fronius_ohmpilot_0_http_fronius", 38.9) - assert_state("sensor.state_code_fronius_ohmpilot_0_http_fronius", 0.0) - assert_state( - "sensor.state_message_fronius_ohmpilot_0_http_fronius", "Up and running" - ) + assert_state("sensor.ohmpilot_energy_consumed", 1233295.0) + assert_state("sensor.ohmpilot_power", 0.0) + assert_state("sensor.ohmpilot_temperature_channel_1", 38.9) + assert_state("sensor.ohmpilot_state_code", 0.0) + assert_state("sensor.ohmpilot_state_message", "Up and running") # power_flow - assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 2274.9) - assert_state("sensor.power_battery_fronius_power_flow_0_http_fronius", 0.1591) - assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2459.3092) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100.0 - ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 216.4328 - ) - assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 7.4984) - assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "bidirectional") - assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 7512664.4042) + assert_state("sensor.solarnet_power_grid", 2274.9) + assert_state("sensor.solarnet_power_battery", 0.1591) + assert_state("sensor.solarnet_power_load", -2459.3092) + assert_state("sensor.solarnet_relative_self_consumption", 100.0) + assert_state("sensor.solarnet_power_photovoltaics", 216.4328) + assert_state("sensor.solarnet_relative_autonomy", 7.4984) + assert_state("sensor.solarnet_meter_mode", "bidirectional") + assert_state("sensor.solarnet_energy_total", 7512664.4042) # storage - assert_state("sensor.current_dc_fronius_storage_0_http_fronius", 0.0) - assert_state("sensor.state_of_charge_fronius_storage_0_http_fronius", 4.6) - assert_state("sensor.capacity_maximum_fronius_storage_0_http_fronius", 16588) - assert_state("sensor.temperature_cell_fronius_storage_0_http_fronius", 21.5) - assert_state("sensor.capacity_designed_fronius_storage_0_http_fronius", 16588) - assert_state("sensor.voltage_dc_fronius_storage_0_http_fronius", 0.0) + assert_state("sensor.byd_battery_box_premium_hv_current_dc", 0.0) + assert_state("sensor.byd_battery_box_premium_hv_state_of_charge", 4.6) + assert_state("sensor.byd_battery_box_premium_hv_capacity_maximum", 16588) + assert_state("sensor.byd_battery_box_premium_hv_temperature_cell", 21.5) + assert_state("sensor.byd_battery_box_premium_hv_capacity_designed", 16588) + assert_state("sensor.byd_battery_box_premium_hv_voltage_dc", 0.0) # Devices device_registry = dr.async_get(hass) @@ -486,52 +393,50 @@ async def test_primo_s0(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 40 # logger - assert_state("sensor.cash_factor_fronius_logger_info_0_http_fronius", 1) - assert_state("sensor.co2_factor_fronius_logger_info_0_http_fronius", 0.53) - assert_state("sensor.delivery_factor_fronius_logger_info_0_http_fronius", 1) + assert_state("sensor.solarnet_grid_export_tariff", 1) + assert_state("sensor.solarnet_co2_factor", 0.53) + assert_state("sensor.solarnet_grid_import_tariff", 1) # inverter 1 - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 17114940) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 22504) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 452.3) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 862) - assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 4.23) - assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 7532755.5) - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 3.85) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 223.9) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 60) - assert_state("sensor.led_color_fronius_inverter_1_http_fronius", 2) - assert_state("sensor.led_state_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.primo_5_0_1_energy_total", 17114940) + assert_state("sensor.primo_5_0_1_energy_day", 22504) + assert_state("sensor.primo_5_0_1_voltage_dc", 452.3) + assert_state("sensor.primo_5_0_1_power_ac", 862) + assert_state("sensor.primo_5_0_1_error_code", 0) + assert_state("sensor.primo_5_0_1_current_dc", 4.23) + assert_state("sensor.primo_5_0_1_status_code", 7) + assert_state("sensor.primo_5_0_1_energy_year", 7532755.5) + assert_state("sensor.primo_5_0_1_current_ac", 3.85) + assert_state("sensor.primo_5_0_1_voltage_ac", 223.9) + assert_state("sensor.primo_5_0_1_frequency_ac", 60) + assert_state("sensor.primo_5_0_1_led_color", 2) + assert_state("sensor.primo_5_0_1_led_state", 0) # inverter 2 - assert_state("sensor.energy_total_fronius_inverter_2_http_fronius", 5796010) - assert_state("sensor.energy_day_fronius_inverter_2_http_fronius", 14237) - assert_state("sensor.voltage_dc_fronius_inverter_2_http_fronius", 329.5) - assert_state("sensor.power_ac_fronius_inverter_2_http_fronius", 296) - assert_state("sensor.error_code_fronius_inverter_2_http_fronius", 0) - assert_state("sensor.current_dc_fronius_inverter_2_http_fronius", 0.97) - assert_state("sensor.status_code_fronius_inverter_2_http_fronius", 7) - assert_state("sensor.energy_year_fronius_inverter_2_http_fronius", 3596193.25) - assert_state("sensor.current_ac_fronius_inverter_2_http_fronius", 1.32) - assert_state("sensor.voltage_ac_fronius_inverter_2_http_fronius", 223.6) - assert_state("sensor.frequency_ac_fronius_inverter_2_http_fronius", 60.01) - assert_state("sensor.led_color_fronius_inverter_2_http_fronius", 2) - assert_state("sensor.led_state_fronius_inverter_2_http_fronius", 0) + assert_state("sensor.primo_3_0_1_energy_total", 5796010) + assert_state("sensor.primo_3_0_1_energy_day", 14237) + assert_state("sensor.primo_3_0_1_voltage_dc", 329.5) + assert_state("sensor.primo_3_0_1_power_ac", 296) + assert_state("sensor.primo_3_0_1_error_code", 0) + assert_state("sensor.primo_3_0_1_current_dc", 0.97) + assert_state("sensor.primo_3_0_1_status_code", 7) + assert_state("sensor.primo_3_0_1_energy_year", 3596193.25) + assert_state("sensor.primo_3_0_1_current_ac", 1.32) + assert_state("sensor.primo_3_0_1_voltage_ac", 223.6) + assert_state("sensor.primo_3_0_1_frequency_ac", 60.01) + assert_state("sensor.primo_3_0_1_led_color", 2) + assert_state("sensor.primo_3_0_1_led_state", 0) # meter - assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 1) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", -2216.7487) + assert_state("sensor.s0_meter_at_inverter_1_meter_location", 1) + assert_state("sensor.s0_meter_at_inverter_1_power_real", -2216.7487) # power_flow - assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2218.9349) - assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "vague-meter") - assert_state("sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 1834) - assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 384.9349) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100 - ) - assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 82.6523) - assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 22910919.5) - assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", 36724) - assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", 11128933.25) + assert_state("sensor.solarnet_power_load", -2218.9349) + assert_state("sensor.solarnet_meter_mode", "vague-meter") + assert_state("sensor.solarnet_power_photovoltaics", 1834) + assert_state("sensor.solarnet_power_grid", 384.9349) + assert_state("sensor.solarnet_relative_self_consumption", 100) + assert_state("sensor.solarnet_relative_autonomy", 82.6523) + assert_state("sensor.solarnet_energy_total", 22910919.5) + assert_state("sensor.solarnet_energy_day", 36724) + assert_state("sensor.solarnet_energy_year", 11128933.25) # Devices device_registry = dr.async_get(hass) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 84ca04df3ba..661b3ace38a 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -578,3 +578,35 @@ async def test_manifest_json(hass, frontend_themes, mock_http_client): json = await resp.json() assert json["theme_color"] != DEFAULT_THEME_COLOR + + +async def test_static_path_cache(hass, mock_http_client): + """Test static paths cache.""" + resp = await mock_http_client.get("/lovelace/default_view", allow_redirects=False) + assert resp.status == 404 + + resp = await mock_http_client.get("/frontend_latest/", allow_redirects=False) + assert resp.status == 403 + + resp = await mock_http_client.get( + "/static/icons/favicon.ico", allow_redirects=False + ) + assert resp.status == 200 + + # and again to make sure the cache works + resp = await mock_http_client.get( + "/static/icons/favicon.ico", allow_redirects=False + ) + assert resp.status == 200 + + resp = await mock_http_client.get( + "/static/fonts/roboto/Roboto-Bold.woff2", allow_redirects=False + ) + assert resp.status == 200 + + resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False) + assert resp.status == 404 + + # and again to make sure the cache works + resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False) + assert resp.status == 404 diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index 3749cf039db..3dd9c67bc15 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -8,11 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.garages_amsterdam.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_full_flow(hass: HomeAssistant) -> None: @@ -21,7 +17,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result with patch( @@ -34,7 +30,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "IJDok" assert "result" in result2 assert result2["result"].unique_id == "IJDok" @@ -63,5 +59,5 @@ async def test_error_handling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == reason diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index 8496f0ca5a2..a928f5f5810 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -29,7 +29,7 @@ async def test_duplicate_error(hass, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -38,7 +38,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_step_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -75,7 +75,7 @@ async def test_step_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 592d139f92e..d303e064c1f 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -68,7 +68,7 @@ async def test_form(hass, fakeimg_png, user_flow, mock_create_stream): user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -104,7 +104,7 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -131,7 +131,7 @@ async def test_form_only_stillimage_gif(hass, fakeimg_gif, user_flow): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" @@ -148,7 +148,7 @@ async def test_form_only_svg_whitespace(hass, fakeimgbytes_svg, user_flow): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @respx.mock @@ -175,7 +175,7 @@ async def test_form_only_still_sample(hass, user_flow, image_file): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @respx.mock @@ -186,31 +186,31 @@ async def test_form_only_still_sample(hass, user_flow, image_file): ( "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", "http://localhost:8123/static/icons/favicon-apple-180x180.png", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.FlowResultType.CREATE_ENTRY, None, ), ( "{% if 1 %}https://bla{% else %}https://yo{% endif %}", "https://bla/", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.FlowResultType.CREATE_ENTRY, None, ), ( "http://{{example.org", "http://example.org", - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, {"still_image_url": "template_error"}, ), ( "invalid1://invalid:4\\1", "invalid1://invalid:4%5c1", - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, {"still_image_url": "malformed_url"}, ), ( "relative/urls/are/not/allowed.jpg", "relative/urls/are/not/allowed.jpg", - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, {"still_image_url": "relative_url"}, ), ], @@ -246,7 +246,7 @@ async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream): user_flow["flow_id"], data ) assert "errors" not in result2, f"errors={result2['errors']}" - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -280,7 +280,7 @@ async def test_form_only_stream(hass, fakeimgbytes_jpg, mock_create_stream): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "127_0_0_1" assert result3["options"] == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, @@ -314,7 +314,7 @@ async def test_form_still_and_stream_not_provided(hass, user_flow): CONF_VERIFY_SSL: False, }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "no_still_image_or_stream_url"} @@ -492,7 +492,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # try updating the still image url @@ -503,10 +503,10 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result["flow_id"], user_input=data, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY result3 = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "init" # verify that an invalid template reports the correct UI error. @@ -515,7 +515,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result3["flow_id"], user_input=data, ) - assert result4.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result4.get("type") == data_entry_flow.FlowResultType.FORM assert result4["errors"] == {"still_image_url": "template_error"} # verify that an invalid template reports the correct UI error. @@ -526,7 +526,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream user_input=data, ) - assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5.get("type") == data_entry_flow.FlowResultType.FORM assert result5["errors"] == {"stream_source": "template_error"} # verify that an relative stream url is rejected. @@ -536,7 +536,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result5["flow_id"], user_input=data, ) - assert result6.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result6.get("type") == data_entry_flow.FlowResultType.FORM assert result6["errors"] == {"stream_source": "relative_url"} # verify that an malformed stream url is rejected. @@ -546,7 +546,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result6["flow_id"], user_input=data, ) - assert result7.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result7.get("type") == data_entry_flow.FlowResultType.FORM assert result7["errors"] == {"stream_source": "malformed_url"} @@ -583,7 +583,7 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # try updating the config options @@ -592,7 +592,7 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): result["flow_id"], user_input=data, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" @@ -607,12 +607,12 @@ async def test_import(hass, fakeimg_png): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Yaml Defined Name" await hass.async_block_till_done() # Any name defined in yaml should end up as the entity id. assert hass.states.get("camera.yaml_defined_name") - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT # These above can be deleted after deprecation period is finished. @@ -707,7 +707,7 @@ async def test_use_wallclock_as_timestamps_option( result = await hass.config_entries.options.async_init( mock_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" with patch( "homeassistant.components.generic.async_setup_entry", return_value=True @@ -716,4 +716,4 @@ async def test_use_wallclock_as_timestamps_option( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/generic/test_diagnostics.py b/tests/components/generic/test_diagnostics.py index 2d4e4c536d8..d31503c11c8 100644 --- a/tests/components/generic/test_diagnostics.py +++ b/tests/components/generic/test_diagnostics.py @@ -1,5 +1,8 @@ """Test generic (IP camera) diagnostics.""" +import pytest + from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.generic.diagnostics import redact_url from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -22,3 +25,34 @@ async def test_entry_diagnostics(hass, hass_client, setup_entry): "content_type": "image/jpeg", }, } + + +@pytest.mark.parametrize( + ("url_in", "url_out_expected"), + [ + ( + "http://www.example.com", + "http://www.example.com", + ), + ( + "http://fred:letmein1@www.example.com/image.php?key=secret2", + "http://****:****@www.example.com/****?****=****", + ), + ( + "http://fred@www.example.com/image.php?key=secret2", + "http://****@www.example.com/****?****=****", + ), + ( + "http://fred@www.example.com/image.php", + "http://****@www.example.com/****", + ), + ( + "http://:letmein1@www.example.com", + "http://:****@www.example.com", + ), + ], +) +def test_redact_url(url_in, url_out_expected): + """Test url redaction.""" + url_out = redact_url(url_in) + assert url_out == url_out_expected diff --git a/tests/components/geo_location/test_init.py b/tests/components/geo_location/test_init.py index 00cb2a872d2..f2cb5c6b108 100644 --- a/tests/components/geo_location/test_init.py +++ b/tests/components/geo_location/test_init.py @@ -20,5 +20,5 @@ async def test_event(hass): assert entity.distance is None assert entity.latitude is None assert entity.longitude is None - with pytest.raises(NotImplementedError): + with pytest.raises(AttributeError): assert entity.source is None diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index 56a301e2f3c..a40668c627f 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -17,7 +17,7 @@ from homeassistant.components.geocaching.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_EXTERNAL_STEP +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -63,7 +63,7 @@ async def test_full_flow( }, ) - assert result.get("type") == RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == FlowResultType.EXTERNAL_STEP assert result.get("step_id") == "auth" assert result.get("url") == ( f"{CURRENT_ENVIRONMENT_URLS['authorize_url']}?response_type=code&client_id={CLIENT_ID}" @@ -161,7 +161,7 @@ async def test_oauth_error( "redirect_uri": REDIRECT_URI, }, ) - assert result.get("type") == RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == FlowResultType.EXTERNAL_STEP client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -181,7 +181,7 @@ async def test_oauth_error( ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "oauth_error" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 013450212ed..9959023e8a6 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -157,10 +157,10 @@ async def webhook_id(hass, geofency_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index 9b471051656..4e00f691884 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -25,7 +25,7 @@ async def test_duplicate_error(hass, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -34,7 +34,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -56,7 +56,7 @@ async def test_step_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -81,7 +81,7 @@ async def test_step_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py index 92c25e00927..a4e1f0587b8 100644 --- a/tests/components/geonetnz_volcano/test_config_flow.py +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -32,7 +32,7 @@ async def test_show_form(hass): result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_step_import(hass): "homeassistant.components.geonetnz_volcano.async_setup", return_value=True ): result = await flow.async_step_import(import_config=conf) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -81,7 +81,7 @@ async def test_step_user(hass): "homeassistant.components.geonetnz_volcano.async_setup", return_value=True ): result = await flow.async_step_user(user_input=conf) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 21fc9d8bfda..8ebb514a4d3 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -25,7 +25,7 @@ async def test_show_form(hass): result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -98,7 +98,7 @@ async def test_create_entry(hass): result = await flow.async_step_user(user_input=CONFIG) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Test Name 1" assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 2bf0fac209f..174d70c3ae3 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -12,12 +12,7 @@ from homeassistant.components.github.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_SHOW_PROGRESS, - RESULT_TYPE_SHOW_PROGRESS_DONE, -) +from homeassistant.data_entry_flow import FlowResultType from .common import MOCK_ACCESS_TOKEN @@ -63,7 +58,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "device" - assert result["type"] == RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == FlowResultType.SHOW_PROGRESS assert "flow_id" in result result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -76,7 +71,7 @@ async def test_full_user_flow_implementation( ) assert result["title"] == "" - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_ACCESS_TOKEN] == MOCK_ACCESS_TOKEN assert "options" in result @@ -96,7 +91,7 @@ async def test_flow_with_registration_failure( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result.get("reason") == "could_not_register" @@ -125,10 +120,10 @@ async def test_flow_with_activation_failure( context={"source": config_entries.SOURCE_USER}, ) assert result["step_id"] == "device" - assert result["type"] == RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE assert result["step_id"] == "could_not_register" @@ -144,7 +139,7 @@ async def test_already_configured( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 1b2c2434fab..d996c3af533 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.glances.Glances.get_data", autospec=True): @@ -109,14 +109,14 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={glances.CONF_SCAN_INTERVAL: 10} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { glances.CONF_SCAN_INTERVAL: 10, } diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 669cace729b..5ebc894fab7 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -34,7 +34,7 @@ async def test_flow_user(hass: HomeAssistant): result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -47,7 +47,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -58,7 +58,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -70,7 +70,7 @@ async def test_flow_user_invalid_host(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_host" @@ -82,7 +82,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -97,14 +97,14 @@ async def test_dhcp_discovery(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MANUFACTURER assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -114,7 +114,7 @@ async def test_dhcp_discovery(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -128,7 +128,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -138,7 +138,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_host" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -148,5 +148,5 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py index ce67351c978..e61015a4925 100644 --- a/tests/components/goalzero/test_sensor.py +++ b/tests/components/goalzero/test_sensor.py @@ -85,7 +85,7 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_wifi_strength") + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_strength") assert state.state == "-62" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 713295c0efd..cbe40a2b9cb 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -20,11 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import _mocked_ismartgate_closed_door_response @@ -59,7 +55,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == { "base": "invalid_auth", } @@ -79,7 +75,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -97,7 +93,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -117,7 +113,7 @@ async def test_form_homekit_unique_id_already_setup(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} flow = next( flow @@ -145,7 +141,7 @@ async def test_form_homekit_unique_id_already_setup(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT async def test_form_homekit_ip_address_already_setup(hass): @@ -170,7 +166,7 @@ async def test_form_homekit_ip_address_already_setup(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT async def test_form_homekit_ip_address(hass): @@ -189,7 +185,7 @@ async def test_form_homekit_ip_address(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} data_schema = result["data_schema"] @@ -219,7 +215,7 @@ async def test_discovered_dhcp( ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -231,7 +227,7 @@ async def test_discovered_dhcp( }, ) assert result2 - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -247,7 +243,7 @@ async def test_discovered_dhcp( }, ) assert result3 - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["data"] == { "device": "ismartgate", "ip_address": "1.2.3.4", @@ -272,7 +268,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_init( @@ -282,7 +278,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" result3 = await hass.config_entries.flow.async_init( @@ -292,5 +288,5 @@ async def test_discovered_by_homekit_and_dhcp(hass): ip="1.2.3.4", macaddress="00:00:00:00:00:00", hostname="mock_hostname" ), ) - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" diff --git a/tests/components/goodwe/test_config_flow.py b/tests/components/goodwe/test_config_flow.py index 89dfd68a783..fe1af93a472 100644 --- a/tests/components/goodwe/test_config_flow.py +++ b/tests/components/goodwe/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.goodwe.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -35,7 +31,7 @@ async def test_manual_setup(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -50,7 +46,7 @@ async def test_manual_setup(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -68,7 +64,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -81,7 +77,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -90,7 +86,7 @@ async def test_manual_setup_device_offline(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -103,5 +99,5 @@ async def test_manual_setup_device_offline(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_error"} diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 4e251b4b006..7e668de2891 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,10 +1,10 @@ """Test configuration and mocks for the google integration.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator import datetime import http -from typing import Any, Generator, TypeVar +from typing import Any, TypeVar from unittest.mock import Mock, mock_open, patch from aiohttp.client_exceptions import ClientError @@ -34,10 +34,10 @@ EMAIL_ADDRESS = "user@gmail.com" # the yaml config that overrides the entity name and other settings. A test # can use a fixture to exercise either case. TEST_API_ENTITY = "calendar.we_are_we_are_a_test_calendar" -TEST_API_ENTITY_NAME = "We are, we are, a... Test Calendar" +TEST_API_ENTITY_NAME = "We are, we are, a... test calendar" # Name of the entity when using yaml configuration overrides TEST_YAML_ENTITY = "calendar.backyard_light" -TEST_YAML_ENTITY_NAME = "Backyard Light" +TEST_YAML_ENTITY_NAME = "Backyard light" # A calendar object returned from the API TEST_API_CALENDAR = { @@ -306,9 +306,12 @@ def mock_insert_event( ) -> Callable[[...], None]: """Fixture for capturing event creation.""" - def _expect_result(calendar_id: str = CALENDAR_ID) -> None: + def _expect_result( + calendar_id: str = CALENDAR_ID, exc: ClientError | None = None + ) -> None: aioclient_mock.post( f"{API_BASE_URL}/calendars/{calendar_id}/events", + exc=exc, ) return diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 9a0cc2e47fa..f4129eb0926 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -101,9 +101,7 @@ def upcoming_event_url(entity: str = TEST_ENTITY) -> str: return f"/api/calendars/{entity}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}" -async def test_all_day_event( - hass, mock_events_list_items, mock_token_read, component_setup -): +async def test_all_day_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" week_from_today = dt_util.now().date() + datetime.timedelta(days=7) end_event = week_from_today + datetime.timedelta(days=1) @@ -343,7 +341,7 @@ async def test_update_error( assert state.name == TEST_ENTITY_NAME assert state.state == "on" - # Advance time to avoid throttling + # Advance time to next data update interval now += datetime.timedelta(minutes=30) aioclient_mock.clear_requests() @@ -353,12 +351,12 @@ async def test_update_error( async_fire_time_changed(hass, now) await hass.async_block_till_done() - # No change + # Entity is marked uanvailable due to API failure state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME - assert state.state == "on" + assert state.state == "unavailable" - # Advance time beyond update/throttle point + # Advance time past next coordinator update now += datetime.timedelta(minutes=30) aioclient_mock.clear_requests() @@ -382,7 +380,7 @@ async def test_update_error( async_fire_time_changed(hass, now) await hass.async_block_till_done() - # State updated + # State updated with new API response state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME assert state.state == "off" @@ -425,10 +423,11 @@ async def test_http_event_api_failure( mock_events_list({}, exc=ClientError()) response = await client.get(upcoming_event_url()) - assert response.status == HTTPStatus.OK - # A failure to talk to the server results in an empty list of events - events = await response.json() - assert events == [] + assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR + + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == "unavailable" @pytest.mark.freeze_time("2022-03-27 12:05:00+00:00") @@ -618,7 +617,7 @@ async def test_future_event_update_behavior( # Advance time until event has started now += datetime.timedelta(minutes=60) - now_utc += datetime.timedelta(minutes=30) + now_utc += datetime.timedelta(minutes=60) with patch("homeassistant.util.dt.utcnow", return_value=now_utc), patch( "homeassistant.util.dt.now", return_value=now ): @@ -672,7 +671,6 @@ async def test_future_event_offset_update_behavior( async def test_unique_id( hass, mock_events_list_items, - mock_token_read, component_setup, config_entry, ): @@ -695,7 +693,6 @@ async def test_unique_id( async def test_unique_id_migration( hass, mock_events_list_items, - mock_token_read, component_setup, config_entry, old_unique_id, @@ -751,7 +748,6 @@ async def test_unique_id_migration( async def test_invalid_unique_id_cleanup( hass, mock_events_list_items, - mock_token_read, component_setup, config_entry, mock_calendars_yaml, diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 24ad8a7b769..5c373fb2219 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -242,7 +242,7 @@ async def test_code_error( mock_code_flow: Mock, component_setup: ComponentSetup, ) -> None: - """Test successful creds setup.""" + """Test server error setting up the oauth flow.""" assert await component_setup() with patch( @@ -256,6 +256,25 @@ async def test_code_error( assert result.get("reason") == "oauth_error" +async def test_timeout_error( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, +) -> None: + """Test timeout error setting up the oauth flow.""" + assert await component_setup() + + with patch( + "homeassistant.components.google.api.OAuth2WebServerFlow.step1_get_device_and_user_codes", + side_effect=TimeoutError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "timeout_connect" + + @pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(seconds=50)]) async def test_expired_after_exchange( hass: HomeAssistant, diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index d9b9ec8ed03..f6c1f8c611f 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -8,6 +8,7 @@ import time from typing import Any from unittest.mock import Mock, patch +from aiohttp.client_exceptions import ClientError import pytest import voluptuous as vol @@ -19,8 +20,9 @@ from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT from homeassistant.components.google.calendar import SERVICE_CREATE_EVENT from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import STATE_OFF +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -305,8 +307,8 @@ async def test_multiple_config_entries( state = hass.states.get("calendar.example_calendar_1") assert state - assert state.name == "Example Calendar 1" assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Example calendar 1" config_entry2 = MockConfigEntry( domain=DOMAIN, data=config_entry.data, unique_id="other-address@example.com" @@ -325,7 +327,7 @@ async def test_multiple_config_entries( state = hass.states.get("calendar.example_calendar_2") assert state - assert state.name == "Example Calendar 2" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Example calendar 2" @pytest.mark.parametrize( @@ -676,6 +678,33 @@ async def test_add_event_date_time( } +async def test_add_event_failure( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + mock_insert_event: Callable[[..., dict[str, Any]], None], + setup_config_entry: MockConfigEntry, + add_event_call_service: Callable[dict[str, Any], Awaitable[None]], +) -> None: + """Test service calls with incorrect fields.""" + + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + assert await component_setup() + + mock_insert_event( + calendar_id=CALENDAR_ID, + exc=ClientError(), + ) + + with pytest.raises(HomeAssistantError): + await add_event_call_service( + {"start_date": "2022-05-01", "end_date": "2022-05-01"} + ) + + @pytest.mark.parametrize( "config_entry_token_expiry", [datetime.datetime.max.timestamp() + 1] ) diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 1426c749552..7f6371c1446 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -36,7 +36,7 @@ async def test_minimum_fields(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -44,7 +44,7 @@ async def test_minimum_fields(hass): MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -60,14 +60,14 @@ async def test_invalid_config_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -91,7 +91,7 @@ async def test_options_flow(hass, mock_config): mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -108,7 +108,7 @@ async def test_options_flow(hass, mock_config): CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -144,7 +144,7 @@ async def test_options_flow_departure_time(hass, mock_config): mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -161,7 +161,7 @@ async def test_options_flow_departure_time(hass, mock_config): CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -192,7 +192,7 @@ async def test_dupe(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -204,13 +204,13 @@ async def test_dupe(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -223,4 +223,4 @@ async def test_dupe(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py new file mode 100644 index 00000000000..3baea5e1140 --- /dev/null +++ b/tests/components/govee_ble/__init__.py @@ -0,0 +1,38 @@ +"""Tests for the Govee BLE integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_GOVEE_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +GVH5075_SERVICE_INFO = BluetoothServiceInfo( + name="GVH5075_2762", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={ + 60552: b"\x00\x03A\xc2d\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\x0c" + }, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) + +GVH5177_SERVICE_INFO = BluetoothServiceInfo( + name="GVH5177_2EC8", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={ + 1: b"\x01\x01\x036&dL\x00\x02\x15INTELLI_ROCKS_HWQw\xf2\xff\xc2" + }, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) diff --git a/tests/components/govee_ble/conftest.py b/tests/components/govee_ble/conftest.py new file mode 100644 index 00000000000..382854a5a28 --- /dev/null +++ b/tests/components/govee_ble/conftest.py @@ -0,0 +1,8 @@ +"""Govee session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py new file mode 100644 index 00000000000..a1b9fed3cd7 --- /dev/null +++ b/tests/components/govee_ble/test_config_flow.py @@ -0,0 +1,170 @@ +"""Test the Govee config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.govee_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import GVH5075_SERVICE_INFO, GVH5177_SERVICE_INFO, NOT_GOVEE_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5075_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "H5075_2762" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + +async def test_async_step_bluetooth_not_govee(hass): + """Test discovery via bluetooth not govee.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_GOVEE_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "H5177_2EC8" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "H5177_2EC8" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py new file mode 100644 index 00000000000..75d269ea0ba --- /dev/null +++ b/tests/components/govee_ble/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Govee BLE sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.govee_ble.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import GVH5075_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(GVH5075_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.h5075_2762_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "21.3442" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075_2762 Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index a885699ca05..4d246cff589 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -65,10 +65,10 @@ async def webhook_id(hass, gpslogger_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py index 27d290e3b90..81a379e8fd8 100644 --- a/tests/components/gree/test_config_flow.py +++ b/tests/components/gree/test_config_flow.py @@ -23,10 +23,10 @@ async def test_creating_entry_sets_up_climate(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -52,10 +52,10 @@ async def test_creating_entry_has_no_devices(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 9d6b099557d..4c73e1d5add 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -6,11 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.group import DOMAIN, async_setup_entry from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, - RESULT_TYPE_MENU, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -47,14 +43,14 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_MENU + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type with patch( @@ -70,7 +66,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room" assert result["data"] == {} assert result["options"] == { @@ -136,14 +132,14 @@ async def test_config_flow_hides_members( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_MENU + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.flow.async_configure( @@ -157,7 +153,7 @@ async def test_config_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by @@ -220,7 +216,7 @@ async def test_options( config_entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") == members1 assert "name" not in result["data_schema"].schema @@ -234,7 +230,7 @@ async def test_options( "entities": members2, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -261,14 +257,14 @@ async def test_options( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_MENU + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") is None @@ -318,7 +314,7 @@ async def test_all_options( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": advanced} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.options.async_configure( @@ -327,7 +323,7 @@ async def test_all_options( "entities": members2, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -414,7 +410,7 @@ async def test_options_flow_hides_members( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(group_config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -425,7 +421,7 @@ async def test_options_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index ba52e09296c..f4e499a3eda 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -50,7 +50,7 @@ async def test_show_authenticate_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -68,7 +68,7 @@ async def test_incorrect_login(hass): result["flow_id"], FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -110,7 +110,7 @@ async def test_multiple_plant_ids(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "plant" user_input = {CONF_PLANT_ID: "123456"} @@ -119,7 +119,7 @@ async def test_multiple_plant_ids(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" @@ -144,7 +144,7 @@ async def test_one_plant_on_account(hass): result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index a56aa6355e8..5e14fc07223 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -21,7 +21,7 @@ async def test_duplicate_error(hass, config, config_entry, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -34,7 +34,7 @@ async def test_connect_error(hass, config): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -55,13 +55,13 @@ async def test_step_user(hass, config, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -85,13 +85,13 @@ async def test_step_zeroconf(hass, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -115,7 +115,7 @@ async def test_step_zeroconf_already_in_progress(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -136,13 +136,13 @@ async def test_step_dhcp(hass, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -162,7 +162,7 @@ async def test_step_dhcp_already_in_progress(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -188,7 +188,7 @@ async def test_step_dhcp_already_setup_match_mac(hass): macaddress="aa:bb:cc:dd:ab:cd", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -210,5 +210,5 @@ async def test_step_dhcp_already_setup_match_ip(hass): macaddress="aa:bb:cc:dd:ab:cd", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/guardian/test_diagnostics.py b/tests/components/guardian/test_diagnostics.py index f48c988c907..2269d09b1eb 100644 --- a/tests/components/guardian/test_diagnostics.py +++ b/tests/components/guardian/test_diagnostics.py @@ -1,22 +1,16 @@ """Test Guardian diagnostics.""" from homeassistant.components.diagnostics import REDACTED -from homeassistant.components.guardian import ( - DATA_PAIRED_SENSOR_MANAGER, - DOMAIN, - PairedSensorManager, -) +from homeassistant.components.guardian import DOMAIN, GuardianData from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics(hass, config_entry, hass_client, setup_guardian): """Test config entry diagnostics.""" - paired_sensor_manager: PairedSensorManager = hass.data[DOMAIN][ - config_entry.entry_id - ][DATA_PAIRED_SENSOR_MANAGER] + data: GuardianData = hass.data[DOMAIN][config_entry.entry_id] # Simulate the pairing of a paired sensor: - await paired_sensor_manager.async_pair_sensor("AABBCCDDEEFF") + await data.paired_sensor_manager.async_pair_sensor("AABBCCDDEEFF") assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 93f909d3bd4..5df675a0f05 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -20,7 +20,7 @@ async def test_flow_works(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == EMAIL @@ -38,7 +38,7 @@ async def test_flow_works_with_authcode(hass, aioclient_mock): "authorization_code": "c29tZXJhbmRvbXN0cmluZw==", } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == EMAIL @@ -57,12 +57,12 @@ async def test_flow_works_with_2fa(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2fa" with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_2fa({"2fa": 123456}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == EMAIL @@ -81,7 +81,7 @@ async def test_flow_with_unknown_2fa(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_2fa_method" @@ -100,7 +100,7 @@ async def test_flow_invalid_login(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_login" @@ -119,7 +119,7 @@ async def test_flow_invalid_2fa(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2fa" with patch( @@ -128,5 +128,5 @@ async def test_flow_invalid_2fa(hass, aioclient_mock): ): result = await flow.async_step_2fa({"2fa": 123456}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_2fa" diff --git a/tests/components/hardkernel/test_config_flow.py b/tests/components/hardkernel/test_config_flow.py index f74b4a4e658..309c796fcc3 100644 --- a/tests/components/hardkernel/test_config_flow.py +++ b/tests/components/hardkernel/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.hardkernel.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Hardkernel" assert result["data"] == {} assert result["options"] == {} @@ -53,6 +53,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 7725e9752f5..252cfe8923c 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -201,7 +201,7 @@ async def test_options_flow(hass, mock_hc, mock_write_config): assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -209,7 +209,7 @@ async def test_options_flow(hass, mock_hc, mock_write_config): user_input={"activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4, diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index d1d940671b8..e7a37ab7105 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -18,7 +18,7 @@ async def test_flow_aborts_already_setup(hass, config_entry): flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -27,7 +27,7 @@ async def test_no_host_shows_form(hass): flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -38,7 +38,7 @@ async def test_cannot_connect_shows_error_form(hass, controller): result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "127.0.0.1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_HOST] == "cannot_connect" assert controller.connect.call_count == 1 @@ -54,7 +54,7 @@ async def test_create_entry_when_host_valid(hass, controller): result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == data @@ -70,7 +70,7 @@ async def test_create_entry_when_friendly_name_valid(hass, controller): result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == {CONF_HOST: "127.0.0.1"} @@ -118,7 +118,7 @@ async def test_discovery_flow_aborts_already_setup( flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_ssdp(discovery_data) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -151,5 +151,5 @@ async def test_import_sets_the_unique_id(hass, controller): data={CONF_HOST: "127.0.0.2"}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index c1ce6f823ae..23e3c1c81c7 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -141,7 +141,7 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -154,7 +154,7 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result2["type"] == data_entry_flow.FlowResultType.MENU assert result2["menu_options"] == menu_options @@ -166,7 +166,7 @@ async def test_step_origin_coordinates( menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -178,7 +178,7 @@ async def test_step_origin_coordinates( } }, ) - assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert location_selector_result["type"] == data_entry_flow.FlowResultType.MENU @pytest.mark.usefixtures("valid_response") @@ -189,13 +189,13 @@ async def test_step_origin_entity( menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_entity"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], {"origin_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert entity_selector_result["type"] == data_entry_flow.FlowResultType.MENU @pytest.mark.usefixtures("valid_response") @@ -206,7 +206,7 @@ async def test_step_destination_coordinates( menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_coordinates"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -218,7 +218,9 @@ async def test_step_destination_coordinates( } }, ) - assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + location_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + ) entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -239,13 +241,13 @@ async def test_step_destination_entity( menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_entity"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], {"destination_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entity_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -327,7 +329,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -339,7 +341,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result["type"] == data_entry_flow.FlowResultType.MENU @pytest.mark.usefixtures("valid_response") @@ -350,7 +352,7 @@ async def test_options_flow_arrival_time_step( menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "arrival_time"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -358,7 +360,7 @@ async def test_options_flow_arrival_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, @@ -376,7 +378,7 @@ async def test_options_flow_departure_time_step( menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "departure_time"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -384,7 +386,7 @@ async def test_options_flow_departure_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, @@ -403,7 +405,7 @@ async def test_options_flow_no_time_step( option_init_result["flow_id"], {"next_step_id": "no_time"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert menu_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, @@ -434,7 +436,7 @@ async def test_import_flow_entity_id(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "namespace test_name" entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -476,7 +478,7 @@ async def test_import_flow_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test_name" entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -519,7 +521,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -539,7 +541,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -559,7 +561,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -579,7 +581,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -599,5 +601,5 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index dc9ba128c35..93fbd1bd204 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -7,14 +7,6 @@ import pytest from homeassistant.components.here_travel_time.config_flow import default_options from homeassistant.components.here_travel_time.const import ( - ATTR_DESTINATION, - ATTR_DESTINATION_NAME, - ATTR_DISTANCE, - ATTR_DURATION, - ATTR_DURATION_IN_TRAFFIC, - ATTR_ORIGIN, - ATTR_ORIGIN_NAME, - ATTR_ROUTE, CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, CONF_DESTINATION_ENTITY_ID, @@ -24,7 +16,6 @@ from homeassistant.components.here_travel_time.const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, CONF_UNIT_SYSTEM, DOMAIN, ICON_BICYCLE, @@ -34,18 +25,18 @@ from homeassistant.components.here_travel_time.const import ( ICON_TRUCK, NO_ROUTE_ERROR_MESSAGE, ROUTE_MODE_FASTEST, - TRAFFIC_MODE_DISABLED, TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, - TRAVEL_MODES_VEHICLE, ) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_API_KEY, CONF_MODE, CONF_NAME, @@ -67,62 +58,57 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - "mode,icon,traffic_mode,unit_system,arrival_time,departure_time,expected_state,expected_distance,expected_duration_in_traffic", + "mode,icon,unit_system,arrival_time,departure_time,expected_duration,expected_distance,expected_duration_in_traffic", [ ( TRAVEL_MODE_CAR, ICON_CAR, - TRAFFIC_MODE_ENABLED, "metric", None, None, + "30", + "23.903", "31", - 23.903, - 31.016666666666666, ), ( TRAVEL_MODE_BICYCLE, ICON_BICYCLE, - TRAFFIC_MODE_DISABLED, "metric", None, None, "30", - 23.903, - 30.05, + "23.903", + "30", ), ( TRAVEL_MODE_PEDESTRIAN, ICON_PEDESTRIAN, - TRAFFIC_MODE_DISABLED, "imperial", None, None, "30", - 14.852631013, - 30.05, + "14.852631013", + "30", ), ( TRAVEL_MODE_PUBLIC_TIME_TABLE, ICON_PUBLIC, - TRAFFIC_MODE_DISABLED, "imperial", "08:00:00", None, "30", - 14.852631013, - 30.05, + "14.852631013", + "30", ), ( TRAVEL_MODE_TRUCK, ICON_TRUCK, - TRAFFIC_MODE_ENABLED, "metric", None, "08:00:00", + "30", + "23.903", "31", - 23.903, - 31.016666666666666, ), ], ) @@ -131,11 +117,10 @@ async def test_sensor( hass: HomeAssistant, mode, icon, - traffic_mode, unit_system, arrival_time, departure_time, - expected_state, + expected_duration, expected_distance, expected_duration_in_traffic, ): @@ -153,7 +138,6 @@ async def test_sensor( CONF_NAME: "test", }, options={ - CONF_TRAFFIC_MODE: traffic_mode, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, CONF_ARRIVAL_TIME: arrival_time, CONF_DEPARTURE_TIME: departure_time, @@ -166,44 +150,57 @@ async def test_sensor( hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES + duration = hass.states.get("sensor.test_duration") + assert duration.attributes.get("unit_of_measurement") == TIME_MINUTES assert ( - sensor.attributes.get(ATTR_ATTRIBUTION) + duration.attributes.get(ATTR_ATTRIBUTION) == "With the support of HERE Technologies. All information is provided without warranty of any kind." ) - assert sensor.state == expected_state + assert duration.attributes.get(ATTR_ICON) == icon + assert duration.state == expected_duration - assert sensor.attributes.get(ATTR_DURATION) == 30.05 - assert sensor.attributes.get(ATTR_DISTANCE) == expected_distance - assert sensor.attributes.get(ATTR_ROUTE) == ( + assert ( + hass.states.get("sensor.test_duration_in_traffic").state + == expected_duration_in_traffic + ) + assert hass.states.get("sensor.test_distance").state == expected_distance + assert hass.states.get("sensor.test_route").state == ( "US-29 - K St NW; US-29 - Whitehurst Fwy; " "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == unit_system assert ( - sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == expected_duration_in_traffic + hass.states.get("sensor.test_duration_in_traffic").state + == expected_duration_in_traffic ) - assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( - [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE] + assert hass.states.get("sensor.test_origin").state == "22nd St NW" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == CAR_ORIGIN_LATITUDE ) - assert sensor.attributes.get(ATTR_DESTINATION) == ",".join( - [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE] - ) - assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW" - assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S" - assert sensor.attributes.get(CONF_MODE) == mode - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is ( - traffic_mode == TRAFFIC_MODE_ENABLED + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == CAR_ORIGIN_LONGITUDE ) - assert sensor.attributes.get(ATTR_ICON) == icon + assert hass.states.get("sensor.test_origin").state == "22nd St NW" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == CAR_ORIGIN_LATITUDE + ) + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == CAR_ORIGIN_LONGITUDE + ) - # Test traffic mode disabled for vehicles - if mode in TRAVEL_MODES_VEHICLE: - assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( - ATTR_DURATION_IN_TRAFFIC - ) + assert hass.states.get("sensor.test_destination").state == "Service Rd S" + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LATITUDE) + == CAR_DESTINATION_LATITUDE + ) + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LONGITUDE) + == CAR_DESTINATION_LONGITUDE + ) @pytest.mark.usefixtures("valid_response") @@ -261,7 +258,9 @@ async def test_no_attribution(hass: HomeAssistant): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert hass.states.get("sensor.test").attributes.get(ATTR_ATTRIBUTION) is None + assert ( + hass.states.get("sensor.test_duration").attributes.get(ATTR_ATTRIBUTION) is None + ) async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): @@ -305,8 +304,7 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + assert hass.states.get("sensor.test_distance").state == "23.903" valid_response.assert_called_with( [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE], diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index ef99c3d1c96..4905638a594 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -22,10 +22,10 @@ async def test_creating_entry_sets_up_climate_discovery(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index e6e2a06501a..21cae9f8366 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -46,7 +46,7 @@ async def test_import_flow(hass): data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == USERNAME assert result["data"] == { CONF_USERNAME: USERNAME, @@ -70,7 +70,7 @@ async def test_user_flow(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -94,7 +94,7 @@ async def test_user_flow(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == USERNAME assert result2["data"] == { CONF_USERNAME: USERNAME, @@ -119,7 +119,7 @@ async def test_user_flow_2fa(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -136,7 +136,7 @@ async def test_user_flow_2fa(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -157,7 +157,7 @@ async def test_user_flow_2fa(hass): }, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -185,7 +185,7 @@ async def test_user_flow_2fa(hass): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == USERNAME assert result4["data"] == { CONF_USERNAME: USERNAME, @@ -239,7 +239,7 @@ async def test_reauth_flow(hass): data=mock_config.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -263,7 +263,7 @@ async def test_reauth_flow(hass): assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -298,7 +298,7 @@ async def test_reauth_2fa_flow(hass): data=mock_config.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -338,7 +338,7 @@ async def test_reauth_2fa_flow(hass): assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -370,14 +370,14 @@ async def test_option_flow(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: UPDATED_SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == UPDATED_SCAN_INTERVAL @@ -387,7 +387,7 @@ async def test_user_flow_2fa_send_new_code(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -404,7 +404,7 @@ async def test_user_flow_2fa_send_new_code(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -419,7 +419,7 @@ async def test_user_flow_2fa_send_new_code(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {} @@ -440,7 +440,7 @@ async def test_user_flow_2fa_send_new_code(hass): }, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {} @@ -465,7 +465,7 @@ async def test_user_flow_2fa_send_new_code(hass): ) await hass.async_block_till_done() - assert result5["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result5["title"] == USERNAME assert result5["data"] == { CONF_USERNAME: USERNAME, @@ -507,7 +507,7 @@ async def test_abort_if_existing_entry(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -517,7 +517,7 @@ async def test_user_flow_invalid_username(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -529,7 +529,7 @@ async def test_user_flow_invalid_username(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_username"} @@ -540,7 +540,7 @@ async def test_user_flow_invalid_password(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -552,7 +552,7 @@ async def test_user_flow_invalid_password(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_password"} @@ -564,7 +564,7 @@ async def test_user_flow_no_internet_connection(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -576,7 +576,7 @@ async def test_user_flow_no_internet_connection(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "no_internet_available"} @@ -588,7 +588,7 @@ async def test_user_flow_2fa_no_internet_connection(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -602,7 +602,7 @@ async def test_user_flow_2fa_no_internet_connection(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -615,7 +615,7 @@ async def test_user_flow_2fa_no_internet_connection(hass): {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "no_internet_available"} @@ -626,7 +626,7 @@ async def test_user_flow_2fa_invalid_code(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -640,7 +640,7 @@ async def test_user_flow_2fa_invalid_code(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -652,7 +652,7 @@ async def test_user_flow_2fa_invalid_code(hass): result["flow_id"], {CONF_CODE: MFA_INVALID_CODE}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "invalid_code"} @@ -663,7 +663,7 @@ async def test_user_flow_unknown_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -676,7 +676,7 @@ async def test_user_flow_unknown_error(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -686,7 +686,7 @@ async def test_user_flow_2fa_unknown_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -700,7 +700,7 @@ async def test_user_flow_2fa_unknown_error(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE with patch( @@ -712,7 +712,7 @@ async def test_user_flow_2fa_unknown_error(hass): {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -733,6 +733,6 @@ async def test_user_flow_2fa_unknown_error(hass): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {"base": "unknown"} diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index fa7d3fee8f0..bb1eb8d1897 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -42,7 +42,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" diff --git a/tests/components/home_plus_control/test_config_flow.py b/tests/components/home_plus_control/test_config_flow.py index cdf1f85f187..e86362c3e11 100644 --- a/tests/components/home_plus_control/test_config_flow.py +++ b/tests/components/home_plus_control/test_config_flow.py @@ -47,7 +47,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "auth" assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" @@ -77,7 +77,7 @@ async def test_full_flow( result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Home+ Control" config_data = result["data"] assert config_data["token"]["refresh_token"] == "mock-refresh-token" @@ -109,7 +109,7 @@ async def test_abort_if_entry_in_progress(hass, current_request_with_host): result = await hass.config_entries.flow.async_init( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -134,7 +134,7 @@ async def test_abort_if_entry_exists(hass, current_request_with_host): result = await hass.config_entries.flow.async_init( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -165,7 +165,7 @@ async def test_abort_if_invalid_token( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "auth" assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" @@ -189,5 +189,5 @@ async def test_abort_if_invalid_token( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "oauth_error" diff --git a/tests/components/homeassistant_alerts/__init__.py b/tests/components/homeassistant_alerts/__init__.py new file mode 100644 index 00000000000..e8e83fad6bd --- /dev/null +++ b/tests/components/homeassistant_alerts/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant alerts integration.""" diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_1.json b/tests/components/homeassistant_alerts/fixtures/alerts_1.json new file mode 100644 index 00000000000..381a31d7a5d --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_1.json @@ -0,0 +1,174 @@ +[ + { + "title": "Aladdin Connect is turning off their previous connection method", + "created": "2022-07-14T06:00:00.000Z", + "integrations": [ + { + "package": "aladdin_connect" + } + ], + "homeassistant": { + "package": "homeassistant", + "resolved_in_version": "2022.7" + }, + "filename": "aladdin_connect.markdown", + "alert_url": "https://alerts.home-assistant.io/#aladdin_connect.markdown" + }, + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + { + "package": "hikvisioncam" + } + ], + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Hive shutting down North American Servers", + "created": "2021-11-13T13:58:00.000Z", + "integrations": [ + { + "package": "hive" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "2021.11.0" + }, + "filename": "hive_us.markdown", + "alert_url": "https://alerts.home-assistant.io/#hive_us.markdown" + }, + { + "title": "HomematicIP (EQ-3) blocks public IP addresses, if access to the Cloud is too frequent.", + "created": "2020-12-20T12:00:00.000Z", + "integrations": [ + { + "package": "homematicip_cloud" + } + ], + "packages": [ + { + "package": "homematicip" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.7" + }, + "filename": "homematicip_cloud.markdown", + "alert_url": "https://alerts.home-assistant.io/#homematicip_cloud.markdown" + }, + { + "title": "Logitech no longer accepting API applications", + "created": "2022-05-16T12:00:00.000Z", + "integrations": [ + { + "package": "logi_circle" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/71945", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.92.0" + }, + "filename": "logi_circle.markdown", + "alert_url": "https://alerts.home-assistant.io/#logi_circle.markdown" + }, + { + "title": "New Neato Botvacs Do Not Support Existing API", + "created": "2021-12-20T13:27:00.000Z", + "integrations": [ + { + "package": "neato" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "neato.markdown", + "alert_url": "https://alerts.home-assistant.io/#neato.markdown" + }, + { + "title": "Nest Desktop Auth Deprecation", + "created": "2022-05-12T14:04:00.000Z", + "integrations": [ + { + "package": "nest" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/67662#issuecomment-1144425848", + "filename": "nest.markdown", + "alert_url": "https://alerts.home-assistant.io/#nest.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Haiku Firmware Update Protocol Change", + "created": "2022-04-05T00:00:00.000Z", + "integrations": [ + { + "package": "senseme" + } + ], + "filename": "senseme.markdown", + "alert_url": "https://alerts.home-assistant.io/#senseme.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "The SoChain integration is disabled due to a dependency conflict", + "created": "2022-02-01T00:00:00.000Z", + "integrations": [ + { + "package": "sochain" + } + ], + "filename": "sochain.markdown", + "alert_url": "https://alerts.home-assistant.io/#sochain.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Yeelight-manufactured Xiaomi-branded devices removed Local Control", + "created": "2021-03-29T06:00:00.000Z", + "integrations": [ + { + "package": "yeelight" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.89" + }, + "filename": "yeelight.markdown", + "alert_url": "https://alerts.home-assistant.io/#yeelight.markdown" + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_2.json b/tests/components/homeassistant_alerts/fixtures/alerts_2.json new file mode 100644 index 00000000000..2941d9da143 --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_2.json @@ -0,0 +1,159 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + { + "package": "hikvisioncam" + } + ], + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Hive shutting down North American Servers", + "created": "2021-11-13T13:58:00.000Z", + "integrations": [ + { + "package": "hive" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "2021.11.0" + }, + "filename": "hive_us.markdown", + "alert_url": "https://alerts.home-assistant.io/#hive_us.markdown" + }, + { + "title": "HomematicIP (EQ-3) blocks public IP addresses, if access to the Cloud is too frequent.", + "created": "2020-12-20T12:00:00.000Z", + "integrations": [ + { + "package": "homematicip_cloud" + } + ], + "packages": [ + { + "package": "homematicip" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.7" + }, + "filename": "homematicip_cloud.markdown", + "alert_url": "https://alerts.home-assistant.io/#homematicip_cloud.markdown" + }, + { + "title": "Logitech no longer accepting API applications", + "created": "2022-05-16T12:00:00.000Z", + "integrations": [ + { + "package": "logi_circle" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/71945", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.92.0" + }, + "filename": "logi_circle.markdown", + "alert_url": "https://alerts.home-assistant.io/#logi_circle.markdown" + }, + { + "title": "New Neato Botvacs Do Not Support Existing API", + "created": "2021-12-20T13:27:00.000Z", + "integrations": [ + { + "package": "neato" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "neato.markdown", + "alert_url": "https://alerts.home-assistant.io/#neato.markdown" + }, + { + "title": "Nest Desktop Auth Deprecation", + "created": "2022-05-12T14:04:00.000Z", + "integrations": [ + { + "package": "nest" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/67662#issuecomment-1144425848", + "filename": "nest.markdown", + "alert_url": "https://alerts.home-assistant.io/#nest.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Haiku Firmware Update Protocol Change", + "created": "2022-04-05T00:00:00.000Z", + "integrations": [ + { + "package": "senseme" + } + ], + "filename": "senseme.markdown", + "alert_url": "https://alerts.home-assistant.io/#senseme.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "The SoChain integration is disabled due to a dependency conflict", + "created": "2022-02-01T00:00:00.000Z", + "integrations": [ + { + "package": "sochain" + } + ], + "filename": "sochain.markdown", + "alert_url": "https://alerts.home-assistant.io/#sochain.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Yeelight-manufactured Xiaomi-branded devices removed Local Control", + "created": "2021-03-29T06:00:00.000Z", + "integrations": [ + { + "package": "yeelight" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.89" + }, + "filename": "yeelight.markdown", + "alert_url": "https://alerts.home-assistant.io/#yeelight.markdown" + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json new file mode 100644 index 00000000000..25ce79d7e7c --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json @@ -0,0 +1,27 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_package.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_package.json new file mode 100644 index 00000000000..bcc0a0223ee --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_no_package.json @@ -0,0 +1,33 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + {} + ], + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + } +] diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py new file mode 100644 index 00000000000..a0fb2e8557d --- /dev/null +++ b/tests/components/homeassistant_alerts/test_init.py @@ -0,0 +1,430 @@ +"""Test creating repairs from alerts.""" +from __future__ import annotations + +from datetime import timedelta +import json +from unittest.mock import ANY, patch + +import pytest + +from homeassistant.components.homeassistant_alerts import DOMAIN, UPDATE_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import assert_lists_same, async_fire_time_changed, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +def stub_alert(aioclient_mock, filename): + """Stub an alert.""" + aioclient_mock.get( + f"https://alerts.home-assistant.io/alerts/{filename}", + text=f"""--- +title: Title for {filename} +--- +Content for {filename} +""", + ) + + +@pytest.mark.parametrize( + "ha_version, expected_alerts", + ( + ( + "2022.7.0", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ( + "2022.8.0", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ( + "2021.10.0", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ), +) +async def test_alerts( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, + ha_version, + expected_alerts, +) -> None: + """Test creating issues based on alerts.""" + + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=load_fixture("alerts_1.json", "homeassistant_alerts"), + ) + for alert in expected_alerts: + stub_alert(aioclient_mock, alert[0]) + + activated_components = ( + "aladdin_connect", + "darksky", + "hikvision", + "hikvisioncam", + "hive", + "homematicip_cloud", + "logi_circle", + "neato", + "nest", + "senseme", + "sochain", + ) + for domain in activated_components: + hass.config.components.add(domain) + + with patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "issue_domain": integration, + "learn_more_url": None, + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts + ] + } + + +@pytest.mark.parametrize( + "ha_version, fixture, expected_alerts", + ( + ( + "2022.7.0", + "alerts_no_integrations.json", + [ + ("dark_sky.markdown", "darksky"), + ], + ), + ( + "2022.7.0", + "alerts_no_package.json", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ], + ), + ), +) +async def test_bad_alerts( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, + ha_version, + fixture, + expected_alerts, +) -> None: + """Test creating issues based on alerts.""" + fixture_content = load_fixture(fixture, "homeassistant_alerts") + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=fixture_content, + ) + for alert in json.loads(fixture_content): + stub_alert(aioclient_mock, alert["filename"]) + + activated_components = ( + "darksky", + "hikvision", + "hikvisioncam", + ) + for domain in activated_components: + hass.config.components.add(domain) + + with patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "issue_domain": integration, + "learn_more_url": None, + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts + ] + } + + +async def test_no_alerts( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test creating issues based on alerts.""" + + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text="", + ) + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +@pytest.mark.parametrize( + "ha_version, fixture_1, expected_alerts_1, fixture_2, expected_alerts_2", + ( + ( + "2022.7.0", + "alerts_1.json", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + "alerts_2.json", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ( + "2022.7.0", + "alerts_2.json", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + "alerts_1.json", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ), +) +async def test_alerts_change( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, + ha_version: str, + fixture_1: str, + expected_alerts_1: list[tuple(str, str)], + fixture_2: str, + expected_alerts_2: list[tuple(str, str)], +) -> None: + """Test creating issues based on alerts.""" + fixture_1_content = load_fixture(fixture_1, "homeassistant_alerts") + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=fixture_1_content, + ) + for alert in json.loads(fixture_1_content): + stub_alert(aioclient_mock, alert["filename"]) + + activated_components = ( + "aladdin_connect", + "darksky", + "hikvision", + "hikvisioncam", + "hive", + "homematicip_cloud", + "logi_circle", + "neato", + "nest", + "senseme", + "sochain", + ) + for domain in activated_components: + hass.config.components.add(domain) + + with patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + now = dt_util.utcnow() + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert_lists_same( + msg["result"]["issues"], + [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "issue_domain": integration, + "learn_more_url": None, + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts_1 + ], + ) + + fixture_2_content = load_fixture(fixture_2, "homeassistant_alerts") + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=fixture_2_content, + ) + for alert in json.loads(fixture_2_content): + stub_alert(aioclient_mock, alert["filename"]) + + future = now + UPDATE_INTERVAL + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert_lists_same( + msg["result"]["issues"], + [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "issue_domain": integration, + "learn_more_url": None, + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts_2 + ], + ) diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 2e96b05a919..e6d5da11806 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow" assert result["data"] == {} assert result["options"] == {} @@ -53,6 +53,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index ce0bed0ff52..1b2a0b4e211 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -56,7 +56,7 @@ async def test_setup_in_bridge_mode(hass, mock_get_source_ip): result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" with patch( @@ -74,7 +74,7 @@ async def test_setup_in_bridge_mode(hass, mock_get_source_ip): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY bridge_name = (result3["title"].split(":"))[0] assert bridge_name == SHORT_BRIDGE_NAME assert result3["data"] == { @@ -112,7 +112,7 @@ async def test_setup_in_bridge_mode_name_taken(hass, mock_get_source_ip): result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" with patch( @@ -130,7 +130,7 @@ async def test_setup_in_bridge_mode_name_taken(hass, mock_get_source_ip): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] != SHORT_BRIDGE_NAME assert result3["title"].startswith(SHORT_BRIDGE_NAME) bridge_name = (result3["title"].split(":"))[0] @@ -194,7 +194,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( result["flow_id"], {"include_domains": ["camera", "media_player", "light", "lock", "remote"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" with patch( @@ -212,7 +212,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -257,7 +257,7 @@ async def test_import(hass, mock_get_source_ip): context={"source": config_entries.SOURCE_IMPORT}, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "port_name_in_use" with patch( @@ -273,7 +273,7 @@ async def test_import(hass, mock_get_source_ip): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "othername:56789" assert result2["data"] == { "name": "othername", @@ -296,7 +296,7 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -307,14 +307,14 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "advanced" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -323,7 +323,7 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): user_input={}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [], "mode": "bridge", @@ -351,7 +351,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -362,7 +362,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] assert entities == ["climate.front_gate"] @@ -375,7 +375,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -430,7 +430,7 @@ async def test_options_flow_devices( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -441,7 +441,7 @@ async def test_options_flow_devices( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" entry = entity_reg.async_get("light.ceiling_lights") @@ -461,7 +461,7 @@ async def test_options_flow_devices( user_input={"devices": [device_id]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [device_id], "mode": "bridge", @@ -510,7 +510,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -521,7 +521,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -531,7 +531,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": ["1fabcabcabcabcabcabcabcabcabc"], "mode": "bridge", @@ -567,7 +567,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -578,7 +578,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" entities = result["data_schema"]({})["entities"] @@ -590,7 +590,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -626,7 +626,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -637,7 +637,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] @@ -649,7 +649,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -676,7 +676,7 @@ async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -687,14 +687,14 @@ async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.new"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -723,7 +723,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -734,7 +734,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -743,7 +743,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -751,7 +751,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -769,7 +769,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -780,7 +780,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -789,7 +789,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -797,7 +797,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", @@ -828,7 +828,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -839,7 +839,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -848,7 +848,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): "entities": ["camera.native_h264", "camera.transcode_h264"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -856,7 +856,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -874,7 +874,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["fan", "vacuum", "climate", "camera"], @@ -899,7 +899,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.native_h264", "camera.transcode_h264"], @@ -916,7 +916,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": ["camera.native_h264"], @@ -930,7 +930,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.native_h264": {}}, "filter": { @@ -960,7 +960,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -971,7 +971,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -980,7 +980,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): "entities": ["camera.audio", "camera.no_audio"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -988,7 +988,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): user_input={"camera_audio": ["camera.audio"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1006,7 +1006,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["fan", "vacuum", "climate", "camera"], @@ -1031,7 +1031,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.audio", "camera.no_audio"], @@ -1048,7 +1048,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": [], @@ -1062,7 +1062,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): user_input={"camera_audio": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.audio": {}}, "filter": { @@ -1103,7 +1103,7 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "yaml" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -1111,7 +1111,7 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) @@ -1131,7 +1131,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1151,7 +1151,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "accessory" assert _get_schema_default(result2["data_schema"].schema, "entities") is None @@ -1159,7 +1159,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1177,7 +1177,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["media_player"], @@ -1190,7 +1190,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "accessory" assert ( _get_schema_default(result2["data_schema"].schema, "entities") @@ -1201,7 +1201,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1226,7 +1226,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" # We need to actually setup the config entry or the data @@ -1244,7 +1244,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -1272,7 +1272,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert _get_schema_default(schema, "mode") == "bridge" @@ -1283,14 +1283,14 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou user_input={"domains": ["camera"], "mode": "accessory"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "accessory" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": "camera.tv"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" with patch( @@ -1305,7 +1305,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.tv": {"video_codec": "copy"}}, "mode": "accessory", @@ -1363,7 +1363,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1387,7 +1387,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1409,7 +1409,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( ] }, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1451,7 +1451,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1475,7 +1475,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1491,7 +1491,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( result2["flow_id"], user_input={"entities": ["media_player.tv", "switch.other"]}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1529,7 +1529,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1553,7 +1553,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "include" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1569,7 +1569,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( ] }, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 83afcedd839..f6855ca3cbb 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -501,7 +501,6 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, ): await _async_start_streaming(hass, acc) await _async_reconfigure_stream(hass, acc, session_info, {}) - await _async_stop_stream(hass, acc, session_info) await _async_stop_all_streams(hass, acc) expected_output = ( diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 749bd4b0f07..18367d28f63 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -9,7 +9,7 @@ import os from typing import Any, Final from unittest import mock -from aiohomekit.model import Accessories, Accessory +from aiohomekit.model import Accessories, AccessoriesState, Accessory from aiohomekit.testing import FakeController, FakePairing from homeassistant.components import zeroconf @@ -186,6 +186,13 @@ async def setup_platform(hass): async def setup_test_accessories(hass, accessories): """Load a fake homekit device based on captured JSON profile.""" fake_controller = await setup_platform(hass) + return await setup_test_accessories_with_controller( + hass, accessories, fake_controller + ) + + +async def setup_test_accessories_with_controller(hass, accessories, fake_controller): + """Load a fake homekit device based on captured JSON profile.""" pairing_id = "00:00:00:00:00:00" @@ -218,13 +225,15 @@ async def device_config_changed(hass, accessories): accessories_obj = Accessories() for accessory in accessories: accessories_obj.add_accessory(accessory) - pairing.accessories = accessories_obj + pairing._accessories_state = AccessoriesState( + accessories_obj, pairing.config_num + 1 + ) discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", addresses=["127.0.0.1"], hostname="mock_hostname", - name="TestDevice", + name="TestDevice._hap._tcp.local.", port=8080, properties={ "md": "TestDevice", diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 81688f88a4b..043213ec159 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -37,3 +37,8 @@ def controller(hass): @pytest.fixture(autouse=True) def hk_mock_async_zeroconf(mock_async_zeroconf): """Auto mock zeroconf.""" + + +@pytest.fixture(autouse=True) +def auto_mock_bluetooth(mock_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/homekit_controller/fixtures/ecobee_501.json b/tests/components/homekit_controller/fixtures/ecobee_501.json new file mode 100644 index 00000000000..245abe974de --- /dev/null +++ b/tests/components/homekit_controller/fixtures/ecobee_501.json @@ -0,0 +1,475 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "ECB501", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "My ecobee", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "123456789016", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.7.340214", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 11, + "perms": ["pr", "hd"], + "format": "string", + "value": "4.1;3fac0fb4", + "maxLen": 64 + }, + { + "type": "00000220-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "hd"], + "format": "data", + "value": "u4qz9c7m80Y=" + }, + { + "type": "000000A6-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": ["pr", "ev"], + "format": "uint32", + "value": 0, + "description": "Accessory Flags" + } + ] + }, + { + "iid": 30, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 31, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 16, + "type": "0000004A-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000000F-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Current Heating Cooling State", + "minValue": 0, + "maxValue": 2, + "minStep": 1, + "valid-values": [0, 1, 2] + }, + { + "type": "00000033-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 3, + "description": "Target Heating Cooling State", + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "valid-values": [0, 1, 2, 3] + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr", "ev"], + "format": "float", + "value": 21.3, + "description": "Current Temperature", + "unit": "celsius", + "minValue": 0, + "maxValue": 40.0, + "minStep": 0.1 + }, + { + "type": "00000035-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 25.6, + "description": "Target Temperature", + "unit": "celsius", + "minValue": 7.2, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "00000036-0000-1000-8000-0026BB765291", + "iid": 21, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Temperature Display Units", + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "valid-values": [0, 1] + }, + { + "type": "0000000D-0000-1000-8000-0026BB765291", + "iid": 22, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 25.6, + "description": "Cooling Threshold Temperature", + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "00000012-0000-1000-8000-0026BB765291", + "iid": 23, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 7.2, + "description": "Heating Threshold Temperature", + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 24, + "perms": ["pr", "ev"], + "format": "float", + "value": 55.0, + "description": "Current Relative Humidity", + "unit": "percentage", + "minValue": 0, + "maxValue": 100.0, + "minStep": 1.0 + }, + { + "type": "00000034-0000-1000-8000-0026BB765291", + "iid": 25, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 36.0, + "description": "Target Relative Humidity", + "unit": "percentage", + "minValue": 20.0, + "maxValue": 50.0, + "minStep": 1.0 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 27, + "perms": ["pr"], + "format": "string", + "value": "My ecobee", + "description": "Name", + "maxLen": 64 + }, + { + "type": "000000BF-0000-1000-8000-0026BB765291", + "iid": 75, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Target Fan State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "000000AF-0000-1000-8000-0026BB765291", + "iid": 76, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Current Fan State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "B7DDB9A3-54BB-4572-91D2-F1F5B0510F8C", + "iid": 33, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "E4489BBC-5227-4569-93E5-B345E3E5508F", + "iid": 34, + "perms": ["pr", "pw"], + "format": "float", + "value": 20.6, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "7D381BAA-20F9-40E5-9BE9-AEB92D4BECEF", + "iid": 35, + "perms": ["pr", "pw"], + "format": "float", + "value": 25.6, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "73AAB542-892A-4439-879A-D2A883724B69", + "iid": 36, + "perms": ["pr", "pw"], + "format": "float", + "value": 17.8, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "5DA985F0-898A-4850-B987-B76C6C78D670", + "iid": 37, + "perms": ["pr", "pw"], + "format": "float", + "value": 27.8, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "05B97374-6DC0-439B-A0FA-CA33F612D425", + "iid": 38, + "perms": ["pr", "pw"], + "format": "float", + "value": 18.3, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "A251F6E7-AC46-4190-9C5D-3D06277BDF9F", + "iid": 39, + "perms": ["pr", "pw"], + "format": "float", + "value": 27.8, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "1B300BC2-CFFC-47FF-89F9-BD6CCF5F2853", + "iid": 40, + "perms": ["pw"], + "format": "uint8", + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "1621F556-1367-443C-AF19-82AF018E99DE", + "iid": 41, + "perms": ["pr", "pw"], + "format": "string", + "value": "2014-01-03T00:00:00-04:00Q", + "maxLen": 64 + }, + { + "type": "FA128DE6-9D7D-49A4-B6D8-4E4E234DEE38", + "iid": 48, + "perms": ["pw"], + "format": "bool" + }, + { + "type": "4A6AE4F6-036C-495D-87CC-B3702B437741", + "iid": 49, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 4, + "minStep": 1 + }, + { + "type": "DB7BF261-7042-4194-8BD1-3AA22830AEDD", + "iid": 50, + "perms": ["pr"], + "format": "uint8", + "value": 2, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "41935E3E-B54D-42E9-B8B9-D33C6319F0AF", + "iid": 51, + "perms": ["pr"], + "format": "bool", + "value": false + }, + { + "type": "C35DA3C0-E004-40E3-B153-46655CDD9214", + "iid": 52, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 0, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "48F62AEC-4171-4B4A-8F0E-1EEB6708B3FB", + "iid": 53, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "1B1515F2-CC45-409F-991F-C480987F92C3", + "iid": 54, + "perms": ["pr"], + "format": "string", + "value": "The Hive is humming along. You have no pending alerts or reminders.", + "maxLen": 64 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "My ecobee Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Motion Detected" + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr", "ev"], + "format": "int", + "value": 14, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "My ecobee Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 1, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr", "ev"], + "format": "int", + "value": 44, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/haa_fan.json b/tests/components/homekit_controller/fixtures/haa_fan.json index 14a215d01fe..a144a9501ba 100644 --- a/tests/components/homekit_controller/fixtures/haa_fan.json +++ b/tests/components/homekit_controller/fixtures/haa_fan.json @@ -70,7 +70,7 @@ "perms": ["pr", "pw", "ev"], "ev": true, "format": "bool", - "value": false + "value": true }, { "aid": 1, @@ -83,7 +83,7 @@ "minValue": 0, "maxValue": 3, "minStep": 1, - "value": 3 + "value": 2 } ] }, diff --git a/tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json b/tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json new file mode 100644 index 00000000000..40366ad32c6 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json @@ -0,0 +1,178 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 85899345921, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 137438953473, + "perms": ["pr"], + "format": "string", + "value": "Lutron Electronics Co., Inc", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 141733920769, + "perms": ["pr"], + "format": "string", + "value": "L-BDG2-WH", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 150323855361, + "perms": ["pr"], + "format": "string", + "value": "Smart Bridge 2", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 206158430209, + "perms": ["pr"], + "format": "string", + "value": "12344331", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 352187318273, + "perms": ["pr"], + "format": "string", + "value": "08.08", + "description": "Firmware Revision", + "maxLen": 64 + } + ] + }, + { + "iid": 2, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 236223201282, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 21474836482, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 85899345921, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 137438953473, + "perms": ["pr"], + "format": "string", + "value": "Lutron Electronics Co., Inc", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 141733920769, + "perms": ["pr"], + "format": "string", + "value": "PD-FSQN-XX", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 150323855361, + "perms": ["pr"], + "format": "string", + "value": "Cas\u00e9ta\u00ae Wireless Fan Speed Control", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 206158430209, + "perms": ["pr"], + "format": "string", + "value": "39024290", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 352187318273, + "perms": ["pr"], + "format": "string", + "value": "001.005", + "description": "Firmware Revision", + "maxLen": 64 + } + ] + }, + { + "iid": 2, + "type": "00000040-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000029-0000-1000-8000-0026BB765291", + "iid": 176093659138, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 0, + "description": "Rotation Speed", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 25 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 150323855362, + "perms": ["pr"], + "format": "string", + "value": "Cas\u00e9ta\u00ae Wireless Fan Speed Control", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 158913789954, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": false, + "description": "On" + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/mss425f.json b/tests/components/homekit_controller/fixtures/mss425f.json new file mode 100644 index 00000000000..35766b32d22 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/mss425f.json @@ -0,0 +1,212 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "Meross", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "MSS425F", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "MSS425F-15cc", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "HH41234", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "4.2.3", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.0.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 9, + "perms": ["pr"], + "format": "string", + "value": "2.0.1;16A75", + "maxLen": 64 + } + ] + }, + { + "iid": 10, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 12, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr"], + "format": "string", + "value": "Outlet-1", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 14, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 15, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": ["pr"], + "format": "string", + "value": "Outlet-2", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 18, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr"], + "format": "string", + "value": "Outlet-3", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 21, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 22, + "perms": ["pr"], + "format": "string", + "value": "Outlet-4", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 23, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 24, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 25, + "perms": ["pr"], + "format": "string", + "value": "USB", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 26, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/mss565.json b/tests/components/homekit_controller/fixtures/mss565.json new file mode 100644 index 00000000000..9ecb735dce3 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/mss565.json @@ -0,0 +1,132 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "Meross", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "MSS565", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "MSS565-28da", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "BB1121", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "4.1.9", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.0.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 9, + "perms": ["pr"], + "format": "string", + "value": "2.0.1;16A75", + "maxLen": 64 + } + ] + }, + { + "iid": 10, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 12, + "type": "00000043-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "iid": 14, + "perms": ["pr", "pw", "ev"], + "format": "int", + "value": 67, + "description": "Brightness", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 15, + "perms": ["pr"], + "format": "string", + "value": "Dimmer Switch", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json b/tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json new file mode 100644 index 00000000000..142af6c03a2 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json @@ -0,0 +1,354 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "Nanoleaf", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "NL55", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "Nanoleaf Strip 3B32", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "AAAA011111111111", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "1.4.40", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.2.4", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 9, + "perms": ["pr", "hd"], + "format": "string", + "value": "5.0;dfeceb3a", + "maxLen": 64 + }, + { + "type": "00000220-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "hd"], + "format": "data", + "value": "cf0c2e5a4476e152" + } + ] + }, + { + "iid": 11, + "type": "00000055-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000004C-0000-1000-8000-0026BB765291", + "iid": 34, + "perms": [], + "format": "data", + "description": "Pair Setup" + }, + { + "type": "0000004E-0000-1000-8000-0026BB765291", + "iid": 35, + "perms": [], + "format": "data", + "description": "Pair Verify" + }, + { + "type": "0000004F-0000-1000-8000-0026BB765291", + "iid": 36, + "perms": [], + "format": "uint8", + "description": "Pairing Features" + }, + { + "type": "00000050-0000-1000-8000-0026BB765291", + "iid": 37, + "perms": ["pr", "pw"], + "format": "data", + "value": null, + "description": "Pairing Pairings" + } + ] + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr"], + "format": "string", + "value": "2.2.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 19, + "type": "00000043-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 49, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 51, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 50, + "perms": ["pr"], + "format": "string", + "value": "Nanoleaf Light Strip", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "iid": 52, + "perms": ["pr", "pw", "ev"], + "format": "int", + "value": 100, + "description": "Brightness", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "000000CE-0000-1000-8000-0026BB765291", + "iid": 55, + "perms": ["pr", "pw", "ev"], + "format": "uint32", + "value": 470, + "description": "Color Temperature", + "minValue": 153, + "maxValue": 470, + "minStep": 1 + }, + { + "type": "00000013-0000-1000-8000-0026BB765291", + "iid": 53, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 30.0, + "description": "Hue", + "unit": "arcdegrees", + "minValue": 0.0, + "maxValue": 360.0, + "minStep": 1.0 + }, + { + "type": "0000002F-0000-1000-8000-0026BB765291", + "iid": 54, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 89.0, + "description": "Saturation", + "unit": "percentage", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 1.0 + }, + { + "type": "A28E1902-CFA1-4D37-A10F-0071CEEEEEBD", + "iid": 60, + "perms": ["pr", "pw", "hd"], + "format": "data", + "value": "" + }, + { + "type": "00000143-0000-1000-8000-0026BB765291", + "iid": 56, + "perms": ["pr", "pw"], + "format": "data", + "value": "" + }, + { + "type": "00000144-0000-1000-8000-0026BB765291", + "iid": 57, + "perms": ["pr"], + "format": "data", + "value": "010901013402040100000000000109010137020402000000" + }, + { + "type": "0000024B-0000-1000-8000-0026BB765291", + "iid": 58, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 2 + } + ] + }, + { + "iid": 31, + "type": "00000701-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 113, + "perms": ["pr"], + "format": "data", + "value": null + }, + { + "type": "00000706-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr"], + "format": "string", + "value": "", + "maxLen": 64 + }, + { + "type": "00000702-0000-1000-8000-0026BB765291", + "iid": 115, + "perms": ["pr"], + "format": "uint16", + "value": 31, + "description": "Thread Node Capabilities", + "minValue": 0, + "maxValue": 31 + }, + { + "type": "00000703-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "uint16", + "value": 127, + "description": "Thread Status", + "minValue": 0, + "maxValue": 127 + }, + { + "type": "0000022B-0000-1000-8000-0026BB765291", + "iid": 118, + "perms": ["pr"], + "format": "bool", + "value": false + }, + { + "type": "00000704-0000-1000-8000-0026BB765291", + "iid": 119, + "perms": ["pr", "pw"], + "format": "data", + "value": null + } + ] + }, + { + "iid": 38, + "type": "00000239-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 2564, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "0000023A-0000-1000-8000-0026BB765291", + "iid": 2561, + "perms": ["pr"], + "format": "uint32", + "value": 0, + "minValue": 0, + "maxValue": 67108863 + }, + { + "type": "0000023C-0000-1000-8000-0026BB765291", + "iid": 2562, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "0000024A-0000-1000-8000-0026BB765291", + "iid": 2565, + "perms": ["pr", "ev"], + "format": "uint32", + "value": 1 + } + ] + }, + { + "iid": 43, + "type": "0E9CC677-A71F-8B83-B84D-568278790CB3", + "characteristics": [] + }, + { + "iid": 44, + "type": "6D2AE1C4-9AEA-11EA-BB37-0242AC130002", + "characteristics": [] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index b2428cdc42b..6950f4cb61e 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -39,8 +39,8 @@ async def test_aqara_gateway_setup(hass): devices=[], entities=[ EntityTestInfo( - "alarm_control_panel.aqara_hub_1563", - friendly_name="Aqara Hub-1563", + "alarm_control_panel.aqara_hub_1563_security_system", + friendly_name="Aqara Hub-1563 Security System", unique_id="homekit-0000000123456789-66304", supported_features=SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_HOME @@ -48,8 +48,8 @@ async def test_aqara_gateway_setup(hass): state="disarmed", ), EntityTestInfo( - "light.aqara_hub_1563", - friendly_name="Aqara Hub-1563", + "light.aqara_hub_1563_lightbulb_1563", + friendly_name="Aqara Hub-1563 Lightbulb-1563", unique_id="homekit-0000000123456789-65792", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, @@ -98,8 +98,8 @@ async def test_aqara_gateway_e1_setup(hass): devices=[], entities=[ EntityTestInfo( - "alarm_control_panel.aqara_hub_e1_00a0", - friendly_name="Aqara-Hub-E1-00A0", + "alarm_control_panel.aqara_hub_e1_00a0_security_system", + friendly_name="Aqara-Hub-E1-00A0 Security System", unique_id="homekit-00aa00000a0-16", supported_features=SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_HOME diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py index e6dce42a1f7..daa6d593988 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -7,6 +7,7 @@ service-label-index despite not being linked to a service-label. https://github.com/home-assistant/core/pull/39090 """ +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from tests.components.homekit_controller.common import ( @@ -38,9 +39,10 @@ async def test_aqara_switch_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="sensor.programmable_switch_battery", - friendly_name="Programmable Switch Battery", + entity_id="sensor.programmable_switch_battery_sensor", + friendly_name="Programmable Switch Battery Sensor", unique_id="homekit-111a1111a1a111-5", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="100", ), diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py index 9afe152c7b3..fe3d1ea5efc 100644 --- a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -37,15 +37,16 @@ async def test_arlo_baby_setup(hass): state="idle", ), EntityTestInfo( - entity_id="binary_sensor.arlobabya0", + entity_id="binary_sensor.arlobabya0_motion", unique_id="homekit-00A0000000000-500", - friendly_name="ArloBabyA0", + friendly_name="ArloBabyA0 Motion", state="off", ), EntityTestInfo( entity_id="sensor.arlobabya0_battery", unique_id="homekit-00A0000000000-700", friendly_name="ArloBabyA0 Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="82", ), @@ -53,6 +54,7 @@ async def test_arlo_baby_setup(hass): entity_id="sensor.arlobabya0_humidity", unique_id="homekit-00A0000000000-900", friendly_name="ArloBabyA0 Humidity", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="60.099998", ), @@ -60,6 +62,7 @@ async def test_arlo_baby_setup(hass): entity_id="sensor.arlobabya0_temperature", unique_id="homekit-00A0000000000-1000", friendly_name="ArloBabyA0 Temperature", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=TEMP_CELSIUS, state="24.0", ), @@ -71,9 +74,9 @@ async def test_arlo_baby_setup(hass): state="1", ), EntityTestInfo( - entity_id="light.arlobabya0", + entity_id="light.arlobabya0_nightlight", unique_id="homekit-00A0000000000-1100", - friendly_name="ArloBabyA0", + friendly_name="ArloBabyA0 Nightlight", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, state="off", diff --git a/tests/components/homekit_controller/specific_devices/test_connectsense.py b/tests/components/homekit_controller/specific_devices/test_connectsense.py index 2cbdf924319..fbb95fc3d89 100644 --- a/tests/components/homekit_controller/specific_devices/test_connectsense.py +++ b/tests/components/homekit_controller/specific_devices/test_connectsense.py @@ -59,8 +59,8 @@ async def test_connectsense_setup(hass): state="379.69299", ), EntityTestInfo( - entity_id="switch.inwall_outlet_0394de", - friendly_name="InWall Outlet-0394DE", + entity_id="switch.inwall_outlet_0394de_outlet_a", + friendly_name="InWall Outlet-0394DE Outlet A", unique_id="homekit-1020301376-13", state="on", ), @@ -89,8 +89,8 @@ async def test_connectsense_setup(hass): state="175.85001", ), EntityTestInfo( - entity_id="switch.inwall_outlet_0394de_2", - friendly_name="InWall Outlet-0394DE", + entity_id="switch.inwall_outlet_0394de_outlet_b", + friendly_name="InWall Outlet-0394DE Outlet B", unique_id="homekit-1020301376-25", state="on", ), diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py new file mode 100644 index 00000000000..ca91607bd09 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py @@ -0,0 +1,68 @@ +"""Tests for Ecobee 501.""" + + +from homeassistant.components.climate.const import ( + SUPPORT_FAN_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.const import STATE_ON + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_ecobee501_setup(hass): + """Test that a Ecobee 501 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "ecobee_501.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="My ecobee", + model="ECB501", + manufacturer="ecobee Inc.", + sw_version="4.7.340214", + hw_version="", + serial_number="123456789016", + devices=[], + entities=[ + EntityTestInfo( + entity_id="climate.my_ecobee", + friendly_name="My ecobee", + unique_id="homekit-123456789016-16", + supported_features=( + SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_TARGET_HUMIDITY + | SUPPORT_FAN_MODE + ), + capabilities={ + "hvac_modes": ["off", "heat", "cool", "heat_cool"], + "fan_modes": ["on", "auto"], + "min_temp": 7.2, + "max_temp": 33.3, + "min_humidity": 20, + "max_humidity": 50, + }, + state="heat_cool", + ), + EntityTestInfo( + entity_id="binary_sensor.my_ecobee_occupancy", + friendly_name="My ecobee Occupancy", + unique_id="homekit-123456789016-57", + unit_of_measurement=None, + state=STATE_ON, + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_eve_degree.py b/tests/components/homekit_controller/specific_devices/test_eve_degree.py index 51880bc076a..55377801529 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_degree.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_degree.py @@ -36,6 +36,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_temperature", unique_id="homekit-AA00A0A00000-22", friendly_name="Eve Degree AA11 Temperature", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=TEMP_CELSIUS, state="22.7719116210938", ), @@ -43,6 +44,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_humidity", unique_id="homekit-AA00A0A00000-27", friendly_name="Eve Degree AA11 Humidity", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="59.4818115234375", ), @@ -58,6 +60,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_battery", unique_id="homekit-AA00A0A00000-17", friendly_name="Eve Degree AA11 Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="65", ), diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index 9d5983650d7..39169ea5af9 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -1,6 +1,6 @@ """Make sure that a H.A.A. fan can be setup.""" -from homeassistant.components.fan import SUPPORT_SET_SPEED +from homeassistant.components.fan import ATTR_PERCENTAGE, SUPPORT_SET_SPEED from homeassistant.helpers.entity import EntityCategory from tests.components.homekit_controller.common import ( @@ -18,7 +18,9 @@ async def test_haa_fan_setup(hass): accessories = await setup_accessories_from_file(hass, "haa_fan.json") await setup_test_accessories(hass, accessories) - # FIXME: assert round(state.attributes["percentage_step"], 2) == 33.33 + haa_fan_state = hass.states.get("fan.haa_c718b3") + attributes = haa_fan_state.attributes + assert attributes[ATTR_PERCENTAGE] == 66 await assert_devices_and_entities_created( hass, @@ -55,7 +57,7 @@ async def test_haa_fan_setup(hass): entity_id="fan.haa_c718b3", friendly_name="HAA-C718B3", unique_id="homekit-C718B3-1-8", - state="off", + state="on", supported_features=SUPPORT_SET_SPEED, capabilities={ "preset_modes": None, diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 76d09629064..e64dc8378c5 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -1,5 +1,6 @@ """Tests for handling accessories on a Hue bridge via HomeKit.""" +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from tests.components.homekit_controller.common import ( @@ -41,7 +42,8 @@ async def test_hue_bridge_setup(hass): entities=[ EntityTestInfo( entity_id="sensor.hue_dimmer_switch_battery", - friendly_name="Hue dimmer switch Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, + friendly_name="Hue dimmer switch battery", unique_id="homekit-6623462389072572-644245094400", unit_of_measurement=PERCENTAGE, state="100", diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index 88d483bd5bc..74525af1daf 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -43,8 +43,8 @@ async def test_koogeek_ls1_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="light.koogeek_ls1_20833f", - friendly_name="Koogeek-LS1-20833F", + entity_id="light.koogeek_ls1_20833f_light_strip", + friendly_name="Koogeek-LS1-20833F Light Strip", unique_id="homekit-AAAA011111111111-7", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, @@ -75,7 +75,11 @@ async def test_recover_from_failure(hass, utcnow, failure_cls): pairing.testing.events_enabled = False helper = Helper( - hass, "light.koogeek_ls1_20833f", pairing, accessories[0], config_entry + hass, + "light.koogeek_ls1_20833f_light_strip", + pairing, + accessories[0], + config_entry, ) # Set light state on fake device to off diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index f93adc732ba..bf8c86b7a7d 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -31,8 +31,8 @@ async def test_koogeek_p1eu_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.koogeek_p1_a00aa0", - friendly_name="Koogeek-P1-A00AA0", + entity_id="switch.koogeek_p1_a00aa0_outlet", + friendly_name="Koogeek-P1-A00AA0 outlet", unique_id="homekit-EUCP03190xxxxx48-7", state="off", ), diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index ed940cb6376..8307dc72f22 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -37,11 +37,17 @@ async def test_koogeek_sw2_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.koogeek_sw2_187a91", - friendly_name="Koogeek-SW2-187A91", + entity_id="switch.koogeek_sw2_187a91_switch_1", + friendly_name="Koogeek-SW2-187A91 Switch 1", unique_id="homekit-CNNT061751001372-8", state="off", ), + EntityTestInfo( + entity_id="switch.koogeek_sw2_187a91_switch_2", + friendly_name="Koogeek-SW2-187A91 Switch 2", + unique_id="homekit-CNNT061751001372-11", + state="off", + ), EntityTestInfo( entity_id="sensor.koogeek_sw2_187a91_power", friendly_name="Koogeek-SW2-187A91 Power", diff --git a/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py new file mode 100644 index 00000000000..8961eb414fa --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py @@ -0,0 +1,55 @@ +"""Tests for handling accessories on a Lutron Caseta bridge via HomeKit.""" + +from homeassistant.const import STATE_OFF + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_lutron_caseta_bridge_setup(hass): + """Test that a Lutron Caseta bridge can be correctly setup in HA via HomeKit.""" + accessories = await setup_accessories_from_file(hass, "lutron_caseta_bridge.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="Smart Bridge 2", + model="L-BDG2-WH", + manufacturer="Lutron Electronics Co., Inc", + sw_version="08.08", + hw_version="", + serial_number="12344331", + devices=[ + DeviceTestInfo( + name="Cas\u00e9ta\u00ae Wireless Fan Speed Control", + model="PD-FSQN-XX", + manufacturer="Lutron Electronics Co., Inc", + sw_version="001.005", + hw_version="", + serial_number="39024290", + unique_id="00:00:00:00:00:00:aid:21474836482", + devices=[], + entities=[ + EntityTestInfo( + entity_id="fan.caseta_r_wireless_fan_speed_control", + friendly_name="Caséta® Wireless Fan Speed Control", + unique_id="homekit-39024290-2", + unit_of_measurement=None, + supported_features=1, + state=STATE_OFF, + capabilities={"preset_modes": None}, + ) + ], + ), + ], + entities=[], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_mss425f.py b/tests/components/homekit_controller/specific_devices/test_mss425f.py new file mode 100644 index 00000000000..3fe0ee739e6 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_mss425f.py @@ -0,0 +1,73 @@ +"""Tests for the Meross MSS425f power strip.""" + + +from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.helpers.entity import EntityCategory + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_meross_mss425f_setup(hass): + """Test that a MSS425f can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "mss425f.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="MSS425F-15cc", + model="MSS425F", + manufacturer="Meross", + sw_version="4.2.3", + hw_version="4.0.0", + serial_number="HH41234", + devices=[], + entities=[ + EntityTestInfo( + entity_id="button.mss425f_15cc_identify", + friendly_name="MSS425F-15cc Identify", + unique_id="homekit-HH41234-aid:1-sid:1-cid:2", + entity_category=EntityCategory.DIAGNOSTIC, + state=STATE_UNKNOWN, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_1", + friendly_name="MSS425F-15cc Outlet-1", + unique_id="homekit-HH41234-12", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_2", + friendly_name="MSS425F-15cc Outlet-2", + unique_id="homekit-HH41234-15", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_3", + friendly_name="MSS425F-15cc Outlet-3", + unique_id="homekit-HH41234-18", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_4", + friendly_name="MSS425F-15cc Outlet-4", + unique_id="homekit-HH41234-21", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_usb", + friendly_name="MSS425F-15cc USB", + unique_id="homekit-HH41234-24", + state=STATE_ON, + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_mss565.py b/tests/components/homekit_controller/specific_devices/test_mss565.py new file mode 100644 index 00000000000..0045a5ec507 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_mss565.py @@ -0,0 +1,42 @@ +"""Tests for the Meross MSS565 wall switch.""" + + +from homeassistant.const import STATE_ON + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_meross_mss565_setup(hass): + """Test that a MSS565 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "mss565.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="MSS565-28da", + model="MSS565", + manufacturer="Meross", + sw_version="4.1.9", + hw_version="4.0.0", + serial_number="BB1121", + devices=[], + entities=[ + EntityTestInfo( + entity_id="light.mss565_28da_dimmer_switch", + friendly_name="MSS565-28da Dimmer Switch", + unique_id="homekit-BB1121-12", + capabilities={"supported_color_modes": ["brightness"]}, + state=STATE_ON, + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_mysa_living.py b/tests/components/homekit_controller/specific_devices/test_mysa_living.py index 5829bd4e165..1a3bcdb2271 100644 --- a/tests/components/homekit_controller/specific_devices/test_mysa_living.py +++ b/tests/components/homekit_controller/specific_devices/test_mysa_living.py @@ -32,8 +32,8 @@ async def test_mysa_living_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="climate.mysa_85dda9", - friendly_name="Mysa-85dda9", + entity_id="climate.mysa_85dda9_thermostat", + friendly_name="Mysa-85dda9 Thermostat", unique_id="homekit-AAAAAAA000-20", supported_features=SUPPORT_TARGET_TEMPERATURE, capabilities={ @@ -60,8 +60,8 @@ async def test_mysa_living_setup(hass): state="24.1", ), EntityTestInfo( - entity_id="light.mysa_85dda9", - friendly_name="Mysa-85dda9", + entity_id="light.mysa_85dda9_display", + friendly_name="Mysa-85dda9 Display", unique_id="homekit-AAAAAAA000-40", supported_features=0, capabilities={"supported_color_modes": ["brightness"]}, diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py new file mode 100644 index 00000000000..7e6a9bb672b --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -0,0 +1,55 @@ +"""Make sure that Nanoleaf NL55 works with BLE.""" + +from homeassistant.helpers.entity import EntityCategory + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + +LIGHT_ON = ("lightbulb", "on") + + +async def test_nanoleaf_nl55_setup(hass): + """Test that a Nanoleaf NL55 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "nanoleaf_strip_nl55.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="Nanoleaf Strip 3B32", + model="NL55", + manufacturer="Nanoleaf", + sw_version="1.4.40", + hw_version="1.2.4", + serial_number="AAAA011111111111", + devices=[], + entities=[ + EntityTestInfo( + entity_id="light.nanoleaf_strip_3b32_nanoleaf_light_strip", + friendly_name="Nanoleaf Strip 3B32 Nanoleaf Light Strip", + unique_id="homekit-AAAA011111111111-19", + supported_features=0, + capabilities={ + "max_mireds": 470, + "min_mireds": 153, + "supported_color_modes": ["color_temp", "hs"], + }, + state="on", + ), + EntityTestInfo( + entity_id="button.nanoleaf_strip_3b32_identify", + friendly_name="Nanoleaf Strip 3B32 Identify", + unique_id="homekit-AAAA011111111111-aid:1-sid:1-cid:2", + entity_category=EntityCategory.DIAGNOSTIC, + state="unknown", + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index 51ebbfdc345..155eb1c5c27 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -5,6 +5,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, ) +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from tests.components.homekit_controller.common import ( @@ -45,15 +46,16 @@ async def test_ryse_smart_bridge_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.master_bath_south", - friendly_name="Master Bath South", + entity_id="cover.master_bath_south_ryse_shade", + friendly_name="Master Bath South RYSE Shade", unique_id="homekit-00:00:00:00:00:00-2-48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), EntityTestInfo( - entity_id="sensor.master_bath_south_battery", - friendly_name="Master Bath South Battery", + entity_id="sensor.master_bath_south_ryse_shade_battery", + friendly_name="Master Bath South RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="100", @@ -71,15 +73,16 @@ async def test_ryse_smart_bridge_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.ryse_smartshade", - friendly_name="RYSE SmartShade", + entity_id="cover.ryse_smartshade_ryse_shade", + friendly_name="RYSE SmartShade RYSE Shade", unique_id="homekit-00:00:00:00:00:00-3-48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), EntityTestInfo( - entity_id="sensor.ryse_smartshade_battery", - friendly_name="RYSE SmartShade Battery", + entity_id="sensor.ryse_smartshade_ryse_shade_battery", + friendly_name="RYSE SmartShade RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -120,15 +123,16 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.lr_left", - friendly_name="LR Left", + entity_id="cover.lr_left_ryse_shade", + friendly_name="LR Left RYSE Shade", unique_id="homekit-00:00:00:00:00:00-2-48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), EntityTestInfo( - entity_id="sensor.lr_left_battery", - friendly_name="LR Left Battery", + entity_id="sensor.lr_left_ryse_shade_battery", + friendly_name="LR Left RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="89", @@ -146,15 +150,16 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.lr_right", - friendly_name="LR Right", + entity_id="cover.lr_right_ryse_shade", + friendly_name="LR Right RYSE Shade", unique_id="homekit-00:00:00:00:00:00-3-48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), EntityTestInfo( - entity_id="sensor.lr_right_battery", - friendly_name="LR Right Battery", + entity_id="sensor.lr_right_ryse_shade_battery", + friendly_name="LR Right RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -172,15 +177,16 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.br_left", - friendly_name="BR Left", + entity_id="cover.br_left_ryse_shade", + friendly_name="BR Left RYSE Shade", unique_id="homekit-00:00:00:00:00:00-4-48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), EntityTestInfo( - entity_id="sensor.br_left_battery", - friendly_name="BR Left Battery", + entity_id="sensor.br_left_ryse_shade_battery", + friendly_name="BR Left RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-4-64", unit_of_measurement=PERCENTAGE, state="100", @@ -198,15 +204,16 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.rzss", - friendly_name="RZSS", + entity_id="cover.rzss_ryse_shade", + friendly_name="RZSS RYSE Shade", unique_id="homekit-00:00:00:00:00:00-5-48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), EntityTestInfo( - entity_id="sensor.rzss_battery", - friendly_name="RZSS Battery", + entity_id="sensor.rzss_ryse_shade_battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, + friendly_name="RZSS RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-5-64", unit_of_measurement=PERCENTAGE, state="0", diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py index d3531e1c65f..f160169a43c 100644 --- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -34,8 +34,8 @@ async def test_simpleconnect_fan_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="fan.simpleconnect_fan_06f674", - friendly_name="SIMPLEconnect Fan-06F674", + entity_id="fan.simpleconnect_fan_06f674_hunter_fan", + friendly_name="SIMPLEconnect Fan-06F674 Hunter Fan", unique_id="homekit-1234567890abcd-8", supported_features=SUPPORT_DIRECTION | SUPPORT_SET_SPEED, capabilities={ diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py index d8d73709c49..07c35fb867d 100644 --- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, ) +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, @@ -52,8 +53,8 @@ async def test_velux_cover_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.velux_window", - friendly_name="VELUX Window", + entity_id="cover.velux_window_roof_window", + friendly_name="VELUX Window Roof Window", unique_id="homekit-1111111a114a111a-8", supported_features=SUPPORT_CLOSE | SUPPORT_SET_POSITION @@ -73,22 +74,25 @@ async def test_velux_cover_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="sensor.velux_sensor_temperature", - friendly_name="VELUX Sensor Temperature", + entity_id="sensor.velux_sensor_temperature_sensor", + friendly_name="VELUX Sensor Temperature sensor", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-a11b111-8", unit_of_measurement=TEMP_CELSIUS, state="18.9", ), EntityTestInfo( - entity_id="sensor.velux_sensor_humidity", - friendly_name="VELUX Sensor Humidity", + entity_id="sensor.velux_sensor_humidity_sensor", + friendly_name="VELUX Sensor Humidity sensor", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-a11b111-11", unit_of_measurement=PERCENTAGE, state="58", ), EntityTestInfo( - entity_id="sensor.velux_sensor_co2", - friendly_name="VELUX Sensor CO2", + entity_id="sensor.velux_sensor_carbon_dioxide_sensor", + friendly_name="VELUX Sensor Carbon Dioxide sensor", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-a11b111-14", unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state="400", diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index 6fa9ff63690..f788b016ba2 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -46,8 +46,8 @@ async def test_vocolinc_flowerbud_setup(hass): state="off", ), EntityTestInfo( - entity_id="light.vocolinc_flowerbud_0d324b", - friendly_name="VOCOlinc-Flowerbud-0d324b", + entity_id="light.vocolinc_flowerbud_0d324b_mood_light", + friendly_name="VOCOlinc-Flowerbud-0d324b Mood Light", unique_id="homekit-AM01121849000327-9", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py index da69b7fe309..3a3579b8781 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py @@ -31,8 +31,8 @@ async def test_vocolinc_vp3_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.vocolinc_vp3_123456", - friendly_name="VOCOlinc-VP3-123456", + entity_id="switch.vocolinc_vp3_123456_outlet", + friendly_name="VOCOlinc-VP3-123456 Outlet", unique_id="homekit-EU0121203xxxxx07-48", state="on", ), diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 646804a86d6..c750c428437 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -10,6 +10,7 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.climate.const import ( DOMAIN, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_SWING_MODE, @@ -32,6 +33,9 @@ def create_thermostat_service(accessory): char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT) char.value = 0 + char = service.add_char(CharacteristicsTypes.FAN_STATE_TARGET) + char.value = 0 + char = service.add_char(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD) char.minValue = 15 char.maxValue = 40 @@ -144,6 +148,32 @@ async def test_climate_change_thermostat_state(hass, utcnow): }, ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {"entity_id": "climate.testdevice", "fan_mode": "on"}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.FAN_STATE_TARGET: 0, + }, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {"entity_id": "climate.testdevice", "fan_mode": "auto"}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.FAN_STATE_TARGET: 1, + }, + ) + async def test_climate_check_min_max_values_per_mode(hass, utcnow): """Test that we we get the appropriate min/max values for each mode.""" diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 3b106dab186..78d3c609a9c 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,8 +1,7 @@ """Tests for homekit_controller config flow.""" import asyncio -from unittest import mock import unittest.mock -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import aiohomekit from aiohomekit.exceptions import AuthenticationError @@ -15,8 +14,13 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_FORM, + FlowResultType, +) from homeassistant.helpers import device_registry +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from tests.common import MockConfigEntry, mock_device_registry @@ -78,23 +82,55 @@ VALID_PAIRING_CODES = [ " 98765432 ", ] +NOT_HK_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="FakeAccessory", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + manufacturer_data={12: b"\x06\x12\x34"}, + service_data={}, + service_uuids=[], + source="local", +) -def _setup_flow_handler(hass, pairing=None): - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass - flow.context = {} +HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED = BluetoothServiceInfo( + name="Eve Energy Not Found", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + # ID is '9b:86:af:01:af:db' + manufacturer_data={ + 76: b"\x061\x01\x9b\x86\xaf\x01\xaf\xdb\x07\x00\x06\x00\x02\x02X\x19\xb1Q" + }, + service_data={}, + service_uuids=[], + source="local", +) - finish_pairing = unittest.mock.AsyncMock(return_value=pairing) +HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED = BluetoothServiceInfo( + name="Eve Energy Found Unpaired", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + # ID is '00:00:00:00:00:00', pairing flag is byte 3 + manufacturer_data={ + 76: b"\x061\x01\x00\x00\x00\x00\x00\x00\x07\x00\x06\x00\x02\x02X\x19\xb1Q" + }, + service_data={}, + service_uuids=[], + source="local", +) - discovery = mock.Mock() - discovery.description.id = "00:00:00:00:00:00" - discovery.async_start_pairing = unittest.mock.AsyncMock(return_value=finish_pairing) - flow.controller = mock.Mock() - flow.controller.pairings = {} - flow.controller.async_find = unittest.mock.AsyncMock(return_value=discovery) - - return flow +HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED = BluetoothServiceInfo( + name="Eve Energy Found Paired", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + # ID is '00:00:00:00:00:00', pairing flag is byte 3 + manufacturer_data={ + 76: b"\x061\x00\x00\x00\x00\x00\x00\x00\x07\x00\x06\x00\x02\x02X\x19\xb1Q" + }, + service_data={}, + service_uuids=[], + source="local", +) @pytest.mark.parametrize("pairing_code", INVALID_PAIRING_CODES) @@ -141,7 +177,7 @@ def get_device_discovery_info( result = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", hostname=device.description.name, - name=device.description.name, + name=device.description.name + "._hap._tcp.local.", addresses=["127.0.0.1"], port=8080, properties={ @@ -151,7 +187,7 @@ def get_device_discovery_info( "c#": device.description.config_num, "s#": device.description.state_num, "ff": "0", - "ci": "0", + "ci": "7", "sf": "0" if paired else "1", "sh": "", }, @@ -208,7 +244,7 @@ async def test_discovery_works(hass, controller, upper_case_props, missing_cshar assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_ZEROCONF, - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", } @@ -265,6 +301,23 @@ async def test_pair_already_paired_1(hass, controller): assert result["reason"] == "already_paired" +async def test_unknown_domain_type(hass, controller): + """Test that aiohomekit can reject discoveries it doesn't support.""" + device = setup_mock_accessory(controller) + # Flag device as already paired + discovery_info = get_device_discovery_info(device) + discovery_info.name = "TestDevice._music._tap.local." + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + assert result["type"] == "abort" + assert result["reason"] == "ignored_model" + + async def test_id_missing(hass, controller): """Test id is missing.""" device = setup_mock_accessory(controller) @@ -475,7 +528,7 @@ async def test_discovery_already_configured_update_csharp(hass, controller): connection_mock = AsyncMock() connection_mock.pairing.connect.reconnect_soon = AsyncMock() - connection_mock.async_refresh_entity_map = AsyncMock() + connection_mock.async_notify_config_changed = MagicMock() hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} device = setup_mock_accessory(controller) @@ -498,7 +551,7 @@ async def test_discovery_already_configured_update_csharp(hass, controller): assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryPort"] == discovery_info.port - assert connection_mock.async_refresh_entity_map.await_count == 1 + assert connection_mock.async_notify_config_changed.call_count == 1 @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) @@ -575,7 +628,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -590,7 +643,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -623,7 +676,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -636,7 +689,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected assert result["type"] == "form" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -663,7 +716,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -676,7 +729,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) assert result["type"] == "form" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -689,7 +742,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, "pairing": True, @@ -720,7 +773,7 @@ async def test_user_works(hass, controller): assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, "unique_id": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Other"}, } result = await hass.config_entries.flow.async_configure( @@ -755,7 +808,7 @@ async def test_user_pairing_with_insecure_setup_code(hass, controller): assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, "unique_id": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Other"}, } result = await hass.config_entries.flow.async_configure( @@ -812,7 +865,7 @@ async def test_unignore_works(hass, controller): assert result["type"] == "form" assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Other"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_UNIGNORE, } @@ -900,7 +953,7 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -925,7 +978,7 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): assert result["type"] == "form" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -943,10 +996,105 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info_paired, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_paired" mdns_update_to_paired.set() result = await task - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == {} + + +async def test_discovery_no_bluetooth_support(hass, controller): + """Test discovery with bluetooth support not available.""" + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + False, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ignored_model" + + +async def test_bluetooth_not_homekit(hass, controller): + """Test bluetooth discovery with a non-homekit device.""" + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_HK_BLUETOOTH_SERVICE_INFO, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ignored_model" + + +async def test_bluetooth_valid_device_no_discovery(hass, controller): + """Test bluetooth discovery with a homekit device and discovery fails.""" + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "accessory_not_found_error" + + +async def test_bluetooth_valid_device_discovery_paired(hass, controller): + """Test bluetooth discovery with a homekit device and discovery works.""" + setup_mock_accessory(controller) + + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_paired" + + +async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): + """Test bluetooth discovery with a homekit device and discovery works.""" + setup_mock_accessory(controller) + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pair" + + assert get_flow_context(hass, result) == { + "source": config_entries.SOURCE_BLUETOOTH, + "unique_id": "AA:BB:CC:DD:EE:FF", + "title_placeholders": {"name": "TestDevice", "category": "Other"}, + } + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2["type"] == RESULT_TYPE_FORM + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={"pairing_code": "111-22-333"} + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Koogeek-LS1-20833F" + assert result3["data"] == {} diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index 72ef571e214..dd0e35b0d1e 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -28,6 +28,7 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "version": 1, "data": {"AccessoryPairingID": "00:00:00:00:00:00"}, }, + "config-num": 0, "entity-map": [ { "aid": 1, @@ -234,28 +235,6 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "sw_version": "2.2.15", "hw_version": "", "entities": [ - { - "original_name": "Koogeek-LS1-20833F", - "disabled": False, - "disabled_by": None, - "entity_category": None, - "device_class": None, - "original_device_class": None, - "icon": None, - "original_icon": None, - "unit_of_measurement": None, - "state": { - "entity_id": "light.koogeek_ls1_20833f", - "state": "off", - "attributes": { - "supported_color_modes": ["hs"], - "friendly_name": "Koogeek-LS1-20833F", - "supported_features": 0, - }, - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", - }, - }, { "device_class": None, "disabled": False, @@ -276,6 +255,28 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc }, "unit_of_measurement": None, }, + { + "device_class": None, + "disabled": False, + "disabled_by": None, + "entity_category": None, + "icon": None, + "original_device_class": None, + "original_icon": None, + "original_name": "Koogeek-LS1-20833F Light Strip", + "state": { + "attributes": { + "friendly_name": "Koogeek-LS1-20833F Light Strip", + "supported_color_modes": ["hs"], + "supported_features": 0, + }, + "entity_id": "light.koogeek_ls1_20833f_light_strip", + "last_changed": "2023-01-01T00:00:00+00:00", + "last_updated": "2023-01-01T00:00:00+00:00", + "state": "off", + }, + "unit_of_measurement": None, + }, ], } ], @@ -299,6 +300,7 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "version": 1, "data": {"AccessoryPairingID": "00:00:00:00:00:00"}, }, + "config-num": 0, "entity-map": [ { "aid": 1, @@ -504,28 +506,6 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "sw_version": "2.2.15", "hw_version": "", "entities": [ - { - "original_name": "Koogeek-LS1-20833F", - "disabled": False, - "disabled_by": None, - "entity_category": None, - "device_class": None, - "original_device_class": None, - "icon": None, - "original_icon": None, - "unit_of_measurement": None, - "state": { - "entity_id": "light.koogeek_ls1_20833f", - "state": "off", - "attributes": { - "supported_color_modes": ["hs"], - "friendly_name": "Koogeek-LS1-20833F", - "supported_features": 0, - }, - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", - }, - }, { "device_class": None, "disabled": False, @@ -536,7 +516,9 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "original_icon": None, "original_name": "Koogeek-LS1-20833F Identify", "state": { - "attributes": {"friendly_name": "Koogeek-LS1-20833F Identify"}, + "attributes": { + "friendly_name": "Koogeek-LS1-20833F " "Identify" + }, "entity_id": "button.koogeek_ls1_20833f_identify", "last_changed": "2023-01-01T00:00:00+00:00", "last_updated": "2023-01-01T00:00:00+00:00", @@ -544,6 +526,28 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): }, "unit_of_measurement": None, }, + { + "device_class": None, + "disabled": False, + "disabled_by": None, + "entity_category": None, + "icon": None, + "original_device_class": None, + "original_icon": None, + "original_name": "Koogeek-LS1-20833F Light Strip", + "state": { + "attributes": { + "friendly_name": "Koogeek-LS1-20833F Light Strip", + "supported_color_modes": ["hs"], + "supported_features": 0, + }, + "entity_id": "light.koogeek_ls1_20833f_light_strip", + "last_changed": "2023-01-01T00:00:00+00:00", + "last_updated": "2023-01-01T00:00:00+00:00", + "state": "off", + }, + "unit_of_measurement": None, + }, ], }, } diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index faaaa2e666f..9d166531562 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -1,4 +1,4 @@ -"""Basic checks for HomeKit motion sensors and contact sensors.""" +"""Basic checks for HomeKit fans.""" from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -41,6 +41,20 @@ def create_fanv2_service(accessory): swing_mode.value = 0 +def create_fanv2_service_non_standard_rotation_range(accessory): + """Define fan v2 with a non-standard rotation range.""" + service = accessory.add_service(ServicesTypes.FAN_V2) + + cur_state = service.add_char(CharacteristicsTypes.ACTIVE) + cur_state.value = 0 + + speed = service.add_char(CharacteristicsTypes.ROTATION_SPEED) + speed.value = 0 + speed.minValue = 0 + speed.maxValue = 3 + speed.minStep = 1 + + def create_fanv2_service_with_min_step(accessory): """Define fan v2 characteristics as per HAP spec.""" service = accessory.add_service(ServicesTypes.FAN_V2) @@ -730,3 +744,64 @@ async def test_v2_oscillate_read(hass, utcnow): ServicesTypes.FAN_V2, {CharacteristicsTypes.SWING_MODE: 1} ) assert state.attributes["oscillating"] is True + + +async def test_v2_set_percentage_non_standard_rotation_range(hass, utcnow): + """Test that we set fan speed with a non-standard rotation range.""" + helper = await setup_test_component( + hass, create_fanv2_service_non_standard_rotation_range + ) + + await helper.async_update(ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: 1}) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 100}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 3, + }, + ) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 66}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 2, + }, + ) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 33}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 1, + }, + ) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 0}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + }, + ) diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 820b89e587d..37d41fcf372 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -1,23 +1,30 @@ """Tests for homekit_controller init.""" +from datetime import timedelta from unittest.mock import patch +from aiohomekit import AccessoryNotFoundError +from aiohomekit.model import Accessory, Transport from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from aiohomekit.testing import FakePairing -from homeassistant.components.homekit_controller.const import ENTITY_MAP -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.components.homekit_controller.const import DOMAIN, ENTITY_MAP +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -from .common import Helper, remove_device +from .common import Helper, remove_device, setup_test_accessories_with_controller +from tests.common import async_fire_time_changed from tests.components.homekit_controller.common import setup_test_component -ALIVE_DEVICE_NAME = "Light Bulb" -ALIVE_DEEVICE_ENTITY_ID = "light.testdevice" +ALIVE_DEVICE_NAME = "testdevice" +ALIVE_DEVICE_ENTITY_ID = "light.testdevice" def create_motion_sensor_service(accessory): @@ -72,7 +79,7 @@ async def test_device_remove_devices(hass, hass_ws_client): entry_id = config_entry.entry_id registry: EntityRegistry = er.async_get(hass) - entity = registry.entities[ALIVE_DEEVICE_ENTITY_ID] + entity = registry.entities[ALIVE_DEVICE_ENTITY_ID] device_registry = dr.async_get(hass) live_device_entry = device_registry.async_get(entity.device_id) @@ -89,3 +96,115 @@ async def test_device_remove_devices(hass, hass_ws_client): await remove_device(await hass_ws_client(hass), dead_device_entry.id, entry_id) is True ) + + +async def test_offline_device_raises(hass, controller): + """Test an offline device raises ConfigEntryNotReady.""" + + is_connected = False + + class OfflineFakePairing(FakePairing): + """Fake pairing that can flip is_connected.""" + + @property + def is_connected(self): + nonlocal is_connected + return is_connected + + @property + def is_available(self): + return self.is_connected + + async def async_populate_accessories_state(self, *args, **kwargs): + nonlocal is_connected + if not is_connected: + raise AccessoryNotFoundError("any") + + async def get_characteristics(self, chars, *args, **kwargs): + nonlocal is_connected + if not is_connected: + raise AccessoryNotFoundError("any") + return {} + + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) + + with patch("aiohomekit.testing.FakePairing", OfflineFakePairing): + await async_setup_component(hass, DOMAIN, {}) + config_entry, _ = await setup_test_accessories_with_controller( + hass, [accessory], controller + ) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + is_connected = True + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert hass.states.get("light.testdevice").state == STATE_OFF + + +async def test_ble_device_only_checks_is_available(hass, controller): + """Test a BLE device only checks is_available.""" + + is_available = False + + class FakeBLEPairing(FakePairing): + """Fake BLE pairing that can flip is_available.""" + + @property + def transport(self): + return Transport.BLE + + @property + def is_connected(self): + return False + + @property + def is_available(self): + nonlocal is_available + return is_available + + async def async_populate_accessories_state(self, *args, **kwargs): + nonlocal is_available + if not is_available: + raise AccessoryNotFoundError("any") + + async def get_characteristics(self, chars, *args, **kwargs): + nonlocal is_available + if not is_available: + raise AccessoryNotFoundError("any") + return {} + + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) + + with patch("aiohomekit.testing.FakePairing", FakeBLEPairing): + await async_setup_component(hass, DOMAIN, {}) + config_entry, _ = await setup_test_accessories_with_controller( + hass, [accessory], controller + ) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + is_available = True + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert hass.states.get("light.testdevice").state == STATE_OFF + + is_available = False + async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + assert hass.states.get("light.testdevice").state == STATE_UNAVAILABLE + + is_available = True + async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + assert hass.states.get("light.testdevice").state == STATE_OFF diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index 39e44c57cc5..83bddf3d26a 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -12,7 +12,7 @@ from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_UNAVAILABLE from tests.components.homekit_controller.common import setup_test_component -LIGHT_BULB_NAME = "Light Bulb" +LIGHT_BULB_NAME = "TestDevice" LIGHT_BULB_ENTITY_ID = "light.testdevice" diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index 78bdb394f0c..6b375b60d9b 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -26,26 +26,6 @@ def create_switch_with_spray_level(accessory): return service -def create_switch_with_ecobee_fan_mode(accessory): - """Define battery level characteristics.""" - service = accessory.add_service(ServicesTypes.OUTLET) - - ecobee_fan_mode = service.add_char( - CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED - ) - - ecobee_fan_mode.value = 0 - ecobee_fan_mode.minStep = 1 - ecobee_fan_mode.minValue = 0 - ecobee_fan_mode.maxValue = 100 - ecobee_fan_mode.format = "float" - - cur_state = service.add_char(CharacteristicsTypes.ON) - cur_state.value = True - - return service - - async def test_read_number(hass, utcnow): """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_spray_level) @@ -106,72 +86,3 @@ async def test_write_number(hass, utcnow): ServicesTypes.OUTLET, {CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: 3}, ) - - -async def test_write_ecobee_fan_mode_number(hass, utcnow): - """Test a switch service that has a sensor characteristic is correctly handled.""" - helper = await setup_test_component(hass, create_switch_with_ecobee_fan_mode) - - # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. - fan_mode = Helper( - hass, - "number.testdevice_fan_mode", - helper.pairing, - helper.accessory, - helper.config_entry, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 1}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 1}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 2}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 2}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 99}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 99}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 100}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 100}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 0}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 0}, - ) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 145d85eeed7..836da1e466f 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -3,7 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode -from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from tests.components.homekit_controller.common import Helper, setup_test_component @@ -79,6 +79,7 @@ async def test_temperature_sensor_read_state(hass, utcnow): assert state.state == "20" assert state.attributes["device_class"] == SensorDeviceClass.TEMPERATURE + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT async def test_temperature_sensor_not_added_twice(hass, utcnow): @@ -146,7 +147,7 @@ async def test_light_level_sensor_read_state(hass, utcnow): async def test_carbon_dioxide_level_sensor_read_state(hass, utcnow): """Test reading the state of a HomeKit carbon dioxide sensor accessory.""" helper = await setup_test_component( - hass, create_carbon_dioxide_level_sensor_service, suffix="co2" + hass, create_carbon_dioxide_level_sensor_service, suffix="carbon_dioxide" ) state = await helper.async_update( diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index b4ed617f901..13d613e3916 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -3,6 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.homekit_controller.const import ENTITY_MAP +from homeassistant.components.homekit_controller.storage import EntityMapStorage from tests.common import flush_store from tests.components.homekit_controller.common import ( @@ -68,7 +69,7 @@ async def test_storage_is_updated_on_add(hass, hass_storage, utcnow): """Test entity map storage is cleaned up on adding an accessory.""" await setup_test_component(hass, create_lightbulb_service) - entity_map = hass.data[ENTITY_MAP] + entity_map: EntityMapStorage = hass.data[ENTITY_MAP] hkid = "00:00:00:00:00:00" # Is in memory store updated? diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 8e3d80ca839..44b91c4ed47 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 258 + assert len(mock_hap.hmip_device_by_entity_id) == 262 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 34c119595b3..823508d5fee 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -340,6 +340,81 @@ async def test_hmip_today_rain_sensor(hass, default_mock_hap_factory): assert ha_state.state == "14.2" +async def test_hmip_temperature_external_sensor_channel_1( + hass, default_mock_hap_factory +): + """Test HomematicipTemperatureDifferenceSensor Channel 1 HmIP-STE2-PCB.""" + entity_id = "sensor.ste2_channel_1_temperature" + entity_name = "STE2 Channel 1 Temperature" + device_model = "HmIP-STE2-PCB" + + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["STE2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) + + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalOne", 25.4) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "25.4" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalOne", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + +async def test_hmip_temperature_external_sensor_channel_2( + hass, default_mock_hap_factory +): + """Test HomematicipTemperatureDifferenceSensor Channel 2 HmIP-STE2-PCB.""" + entity_id = "sensor.ste2_channel_2_temperature" + entity_name = "STE2 Channel 2 Temperature" + device_model = "HmIP-STE2-PCB" + + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["STE2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) + + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalTwo", 22.4) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "22.4" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalTwo", 23.4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.4" + + +async def test_hmip_temperature_external_sensor_delta(hass, default_mock_hap_factory): + """Test HomematicipTemperatureDifferenceSensor Delta HmIP-STE2-PCB.""" + entity_id = "sensor.ste2_delta_temperature" + entity_name = "STE2 Delta Temperature" + device_model = "HmIP-STE2-PCB" + + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["STE2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) + + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalDelta", 0.4) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "0.4" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data( + hass, hmip_device, "temperatureExternalDelta", -0.5 + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "-0.5" + + async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap_factory): """Test HomematicipPassageDetectorDeltaCounter.""" entity_id = "sensor.spdr_1" diff --git a/tests/components/homewizard/fixtures/data.json b/tests/components/homewizard/fixtures/data.json index b6eada38038..35cfd2197a1 100644 --- a/tests/components/homewizard/fixtures/data.json +++ b/tests/components/homewizard/fixtures/data.json @@ -12,5 +12,7 @@ "active_power_l2_w": 456, "active_power_l3_w": 123.456, "total_gas_m3": 1122.333, - "gas_timestamp": 210314112233 + "gas_timestamp": 210314112233, + "active_liter_lpm": 12.345, + "total_liter_m3": 1234.567 } diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index fca00b71892..cdd67cbf7ea 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homewizard.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .generator import get_mock_device @@ -95,7 +91,7 @@ async def test_discovery_flow_works(hass, aioclient_mock): result = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input=None ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with patch( @@ -109,7 +105,7 @@ async def test_discovery_flow_works(hass, aioclient_mock): flow["flow_id"], user_input={"ip_address": "192.168.43.183"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "P1 meter (aabbccddeeff)" assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" @@ -176,7 +172,7 @@ async def test_discovery_disabled_api(hass, aioclient_mock): data=service_info, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.homewizard.async_setup_entry", @@ -189,7 +185,7 @@ async def test_discovery_disabled_api(hass, aioclient_mock): result["flow_id"], user_input={"ip_address": "192.168.43.183"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "api_not_enabled" @@ -218,7 +214,7 @@ async def test_discovery_missing_data_in_service_info(hass, aioclient_mock): data=service_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_discovery_parameters" @@ -247,7 +243,7 @@ async def test_discovery_invalid_api(hass, aioclient_mock): data=service_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unsupported_api_version" @@ -275,7 +271,7 @@ async def test_check_disabled_api(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "api_not_enabled" @@ -303,7 +299,7 @@ async def test_check_error_handling_api(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown_error" @@ -331,7 +327,7 @@ async def test_check_detects_invalid_api(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unsupported_api_version" @@ -359,5 +355,5 @@ async def test_check_requesterror(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown_error" diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index e477c94d914..899bfb5fb2f 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -41,6 +41,8 @@ async def test_diagnostics( "active_power_l3_w": 123.456, "total_gas_m3": 1122.333, "gas_timestamp": "2021-03-14T11:22:33", + "active_liter_lpm": 12.345, + "total_liter_m3": 1234.567, }, "state": {"power_on": True, "switch_lock": False, "brightness": 255}, }, diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 8195aa11708..85b6dc58235 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -61,7 +61,7 @@ async def test_sensor_entity_smr_version( assert state.state == "50" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) DSMR Version" + == "Product Name (aabbccddeeff) DSMR version" ) assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -101,7 +101,7 @@ async def test_sensor_entity_meter_model( assert state.state == "Model X" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Smart Meter Model" + == "Product Name (aabbccddeeff) Smart meter model" ) assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -128,8 +128,8 @@ async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config entity_registry = er.async_get(hass) - state = hass.states.get("sensor.product_name_aabbccddeeff_wifi_ssid") - entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wifi_ssid") + state = hass.states.get("sensor.product_name_aabbccddeeff_wi_fi_ssid") + entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wi_fi_ssid") assert entry assert state assert entry.unique_id == "aabbccddeeff_wifi_ssid" @@ -137,7 +137,7 @@ async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config assert state.state == "My Wifi" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Wifi SSID" + == "Product Name (aabbccddeeff) Wi-Fi SSID" ) assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -166,7 +166,7 @@ async def test_sensor_entity_wifi_strength( entity_registry = er.async_get(hass) - entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wifi_strength") + entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wi_fi_strength") assert entry assert entry.unique_id == "aabbccddeeff_wifi_strength" assert entry.disabled @@ -206,7 +206,7 @@ async def test_sensor_entity_total_power_import_t1_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Import T1" + == "Product Name (aabbccddeeff) Total power import T1" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -248,7 +248,7 @@ async def test_sensor_entity_total_power_import_t2_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Import T2" + == "Product Name (aabbccddeeff) Total power import T2" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -290,7 +290,7 @@ async def test_sensor_entity_total_power_export_t1_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Export T1" + == "Product Name (aabbccddeeff) Total power export T1" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -332,7 +332,7 @@ async def test_sensor_entity_total_power_export_t2_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Export T2" + == "Product Name (aabbccddeeff) Total power export T2" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -370,7 +370,7 @@ async def test_sensor_entity_active_power( assert state.state == "123.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power" + == "Product Name (aabbccddeeff) Active power" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -410,7 +410,7 @@ async def test_sensor_entity_active_power_l1( assert state.state == "123.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power L1" + == "Product Name (aabbccddeeff) Active power L1" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -450,7 +450,7 @@ async def test_sensor_entity_active_power_l2( assert state.state == "456.456" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power L2" + == "Product Name (aabbccddeeff) Active power L2" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -490,7 +490,7 @@ async def test_sensor_entity_active_power_l3( assert state.state == "789.789" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power L3" + == "Product Name (aabbccddeeff) Active power L3" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -526,7 +526,7 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config assert state.state == "50" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Gas" + == "Product Name (aabbccddeeff) Total gas" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS @@ -534,6 +534,88 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config assert ATTR_ICON not in state.attributes +async def test_sensor_entity_active_liters( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active liters (watermeter).""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_liter_lpm": 12.345})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_water_usage") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_water_usage" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_active_liter_lpm" + assert not entry.disabled + assert state.state == "12.345" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active water usage" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "l/min" + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:water" + + +async def test_sensor_entity_total_liters( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads total liters (watermeter).""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"total_liter_m3": 1234.567})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_total_water_usage") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_total_water_usage" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_total_liter_m3" + assert not entry.disabled + assert state.state == "1234.567" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Total water usage" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:gauge" + + async def test_sensor_entity_disabled_when_null( hass, mock_config_entry_data, mock_config_entry ): diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index 118f0774a47..224d32e1b5c 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -39,7 +39,7 @@ async def test_switch_entity_not_loaded_when_not_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state_power_on = hass.states.get("sensor.product_name_aabbccddeeff_switch") + state_power_on = hass.states.get("sensor.product_name_aabbccddeeff") state_switch_lock = hass.states.get("sensor.product_name_aabbccddeeff_switch_lock") assert state_power_on is None @@ -67,10 +67,8 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e entity_registry = er.async_get(hass) - state_power_on = hass.states.get("switch.product_name_aabbccddeeff_switch") - entry_power_on = entity_registry.async_get( - "switch.product_name_aabbccddeeff_switch" - ) + state_power_on = hass.states.get("switch.product_name_aabbccddeeff") + entry_power_on = entity_registry.async_get("switch.product_name_aabbccddeeff") assert state_power_on assert entry_power_on assert entry_power_on.unique_id == "aabbccddeeff_power_on" @@ -78,7 +76,7 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e assert state_power_on.state == STATE_OFF assert ( state_power_on.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Switch" + == "Product Name (aabbccddeeff)" ) assert state_power_on.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_OUTLET assert ATTR_ICON not in state_power_on.attributes @@ -95,7 +93,7 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e assert state_switch_lock.state == STATE_OFF assert ( state_switch_lock.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Switch Lock" + == "Product Name (aabbccddeeff) Switch lock" ) assert state_switch_lock.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SWITCH assert ATTR_ICON not in state_switch_lock.attributes @@ -127,38 +125,30 @@ async def test_switch_power_on_off(hass, mock_config_entry_data, mock_config_ent await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state - == STATE_OFF - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_OFF # Turn power_on on await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, - {"entity_id": "switch.product_name_aabbccddeeff_switch"}, + {"entity_id": "switch.product_name_aabbccddeeff"}, blocking=True, ) await hass.async_block_till_done() assert len(api.state_set.mock_calls) == 1 - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_ON # Turn power_on off await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_OFF, - {"entity_id": "switch.product_name_aabbccddeeff_switch"}, + {"entity_id": "switch.product_name_aabbccddeeff"}, blocking=True, ) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state - == STATE_OFF - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_OFF assert len(api.state_set.mock_calls) == 2 @@ -254,9 +244,7 @@ async def test_switch_lock_sets_power_on_unavailable( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_ON assert ( hass.states.get("switch.product_name_aabbccddeeff_switch_lock").state == STATE_OFF @@ -273,7 +261,7 @@ async def test_switch_lock_sets_power_on_unavailable( await hass.async_block_till_done() assert len(api.state_set.mock_calls) == 1 assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state + hass.states.get("switch.product_name_aabbccddeeff").state == STATE_UNAVAILABLE ) assert ( @@ -290,9 +278,7 @@ async def test_switch_lock_sets_power_on_unavailable( ) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_ON assert ( hass.states.get("switch.product_name_aabbccddeeff_switch_lock").state == STATE_OFF diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index b93e359c1ac..d877133bdcd 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -53,7 +53,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG @@ -70,7 +70,7 @@ async def test_show_option_form( result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" @@ -91,7 +91,7 @@ async def test_create_option_entry( user_input={CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2, diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 5e482d16248..7a4202c1a67 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -15,12 +15,13 @@ import homeassistant.components.http as http from homeassistant.components.http import KEY_AUTHENTICATED from homeassistant.components.http.ban import ( IP_BANS_FILE, - KEY_BANNED_IPS, + KEY_BAN_MANAGER, KEY_FAILED_LOGIN_ATTEMPTS, - IpBan, + IpBanManager, setup_bans, ) from homeassistant.components.http.view import request_handler_factory +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from . import mock_real_ip @@ -58,8 +59,10 @@ async def test_access_from_banned_ip(hass, aiohttp_client): set_real_ip = mock_real_ip(app) with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", - return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS], + "homeassistant.components.http.ban.load_yaml_config_file", + return_value={ + banned_ip: {"banned_at": "2016-11-16T19:20:03"} for banned_ip in BANNED_IPS + }, ): client = await aiohttp_client(app) @@ -69,6 +72,99 @@ async def test_access_from_banned_ip(hass, aiohttp_client): assert resp.status == HTTPStatus.FORBIDDEN +async def test_access_from_banned_ip_with_partially_broken_yaml_file( + hass, aiohttp_client, caplog +): + """Test accessing to server from banned IP. Both trusted and not. + + We inject some garbage into the yaml file to make sure it can + still load the bans. + """ + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + data = {banned_ip: {"banned_at": "2016-11-16T19:20:03"} for banned_ip in BANNED_IPS} + data["5.3.3.3"] = {"banned_at": "garbage"} + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + return_value=data, + ): + client = await aiohttp_client(app) + + for remote_addr in BANNED_IPS: + set_real_ip(remote_addr) + resp = await client.get("/") + assert resp.status == HTTPStatus.FORBIDDEN + + # Ensure garbage data is ignored + set_real_ip("5.3.3.3") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + + assert "Failed to load IP ban" in caplog.text + + +async def test_no_ip_bans_file(hass, aiohttp_client): + """Test no ip bans file.""" + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + side_effect=FileNotFoundError, + ): + client = await aiohttp_client(app) + + set_real_ip("4.3.2.1") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + + +async def test_failure_loading_ip_bans_file(hass, aiohttp_client): + """Test failure loading ip bans file.""" + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + side_effect=HomeAssistantError, + ): + client = await aiohttp_client(app) + + set_real_ip("4.3.2.1") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + + +async def test_ip_ban_manager_never_started(hass, aiohttp_client, caplog): + """Test we handle the ip ban manager not being started.""" + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + side_effect=FileNotFoundError, + ): + client = await aiohttp_client(app) + + # Mock the manager never being started + del app[KEY_BAN_MANAGER] + + set_real_ip("4.3.2.1") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + assert "IP Ban middleware loaded but banned IPs not loaded" in caplog.text + + @pytest.mark.parametrize( "remote_addr, bans, status", list( @@ -95,10 +191,13 @@ async def test_access_from_supervisor_ip( mock_real_ip(app)(remote_addr) with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", return_value=[] + "homeassistant.components.http.ban.load_yaml_config_file", + return_value={}, ): client = await aiohttp_client(app) + manager: IpBanManager = app[KEY_BAN_MANAGER] + assert await async_setup_component(hass, "hassio", {"hassio": {}}) m_open = mock_open() @@ -108,13 +207,13 @@ async def test_access_from_supervisor_ip( ): resp = await client.get("/") assert resp.status == HTTPStatus.UNAUTHORIZED - assert len(app[KEY_BANNED_IPS]) == bans + assert len(manager.ip_bans_lookup) == bans assert m_open.call_count == bans # second request should be forbidden if banned resp = await client.get("/") assert resp.status == status - assert len(app[KEY_BANNED_IPS]) == bans + assert len(manager.ip_bans_lookup) == bans async def test_ban_middleware_not_loaded_by_config(hass): @@ -135,7 +234,7 @@ async def test_ban_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_ip_bans_file_creation(hass, aiohttp_client): +async def test_ip_bans_file_creation(hass, aiohttp_client, caplog): """Testing if banned IP file created.""" app = web.Application() app["hass"] = hass @@ -144,32 +243,35 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): """Return a mock web response.""" raise HTTPUnauthorized - app.router.add_get("/", unauth_handler) + app.router.add_get("/example", unauth_handler) setup_bans(hass, app, 2) mock_real_ip(app)("200.201.202.204") with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", - return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS], + "homeassistant.components.http.ban.load_yaml_config_file", + return_value={ + banned_ip: {"banned_at": "2016-11-16T19:20:03"} for banned_ip in BANNED_IPS + }, ): client = await aiohttp_client(app) + manager: IpBanManager = app[KEY_BAN_MANAGER] m_open = mock_open() with patch("homeassistant.components.http.ban.open", m_open, create=True): - resp = await client.get("/") + resp = await client.get("/example") assert resp.status == HTTPStatus.UNAUTHORIZED - assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) + assert len(manager.ip_bans_lookup) == len(BANNED_IPS) assert m_open.call_count == 0 - resp = await client.get("/") + resp = await client.get("/example") assert resp.status == HTTPStatus.UNAUTHORIZED - assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) + 1 + assert len(manager.ip_bans_lookup) == len(BANNED_IPS) + 1 m_open.assert_called_once_with( hass.config.path(IP_BANS_FILE), "a", encoding="utf8" ) - resp = await client.get("/") + resp = await client.get("/example") assert resp.status == HTTPStatus.FORBIDDEN assert m_open.call_count == 1 @@ -181,6 +283,11 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): == "Login attempt or request with invalid authentication from example.com (200.201.202.204). See the log for details." ) + assert ( + "Login attempt or request with invalid authentication from example.com (200.201.202.204). Requested URL: '/example'." + in caplog.text + ) + async def test_failed_login_attempts_counter(hass, aiohttp_client): """Testing if failed login attempts counter increased.""" diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index b363836cc4f..84f66e8f0ab 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -41,7 +41,7 @@ async def test_show_set_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -54,7 +54,7 @@ async def test_urlize_plain_host(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert user_input[CONF_URL] == f"http://{host}/" @@ -85,7 +85,7 @@ async def test_already_configured(hass, requests_mock, login_requests_mock): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -96,7 +96,7 @@ async def test_connection_error(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "unknown"} @@ -142,7 +142,7 @@ async def test_login_error(hass, login_requests_mock, code, errors): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -164,7 +164,7 @@ async def test_success(hass, login_requests_mock): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -195,7 +195,7 @@ async def test_ssdp(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({})[CONF_URL] == url @@ -209,7 +209,7 @@ async def test_options(hass): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" recipient = "+15555550000" diff --git a/tests/components/huawei_lte/test_switches.py b/tests/components/huawei_lte/test_switches.py new file mode 100644 index 00000000000..5bafed27e70 --- /dev/null +++ b/tests/components/huawei_lte/test_switches.py @@ -0,0 +1,138 @@ +"""Tests for the Huawei LTE switches.""" +from unittest.mock import MagicMock, patch + +from huawei_lte_api.enums.cradle import ConnectionStatusEnum +from pytest import fixture + +from homeassistant.components.huawei_lte.const import DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_URL, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry + +from tests.common import MockConfigEntry + +SWITCH_WIFI_GUEST_NETWORK = "switch.lte_wifi_guest_network" + + +@fixture +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=MagicMock( + device=MagicMock( + information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) + ), + monitoring=MagicMock( + check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), + status=MagicMock( + return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} + ), + ), + wlan=MagicMock( + multi_basic_settings=MagicMock( + return_value={ + "Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]} + } + ), + wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), + ), + ), +) +async def setup_component_with_wifi_guest_network( + client: MagicMock, hass: HomeAssistant +) -> None: + """Initialize huawei_lte components.""" + assert client + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + assert await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + + +@fixture +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=MagicMock( + device=MagicMock( + information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) + ), + monitoring=MagicMock( + check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), + status=MagicMock( + return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} + ), + ), + wlan=MagicMock( + multi_basic_settings=MagicMock(return_value={}), + wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), + ), + ), +) +async def setup_component_without_wifi_guest_network( + client: MagicMock, hass: HomeAssistant +) -> None: + """Initialize huawei_lte components.""" + assert client + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + assert await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + + +def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_not_present( + hass: HomeAssistant, + setup_component_without_wifi_guest_network, +) -> None: + """Test switch wifi guest network config entry when network is not present.""" + entity_registry: EntityRegistry = er.async_get(hass) + assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) + + +def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_present( + hass: HomeAssistant, + setup_component_with_wifi_guest_network, +) -> None: + """Test switch wifi guest network config entry when network is present.""" + entity_registry: EntityRegistry = er.async_get(hass) + assert entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) + + +async def test_turn_on_switch_wifi_guest_network( + hass: HomeAssistant, setup_component_with_wifi_guest_network +) -> None: + """Test switch wifi guest network turn on method.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: SWITCH_WIFI_GUEST_NETWORK}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_ON) + hass.data[DOMAIN].routers[ + "test-serial-number" + ].client.wlan.wifi_guest_network_switch.assert_called_once_with(True) + + +async def test_turn_off_switch_wifi_guest_network( + hass: HomeAssistant, setup_component_with_wifi_guest_network +) -> None: + """Test switch wifi guest network turn off method.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: SWITCH_WIFI_GUEST_NETWORK}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_OFF) + hass.data[DOMAIN].routers[ + "test-serial-number" + ].client.wlan.wifi_guest_network_switch.assert_called_with(False) diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 4b0ae908a2d..8aac11baf6d 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -68,7 +68,7 @@ async def test_form_invalid_auth(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["type"] == data_entry_flow.FlowResultType.FORM assert form_result["errors"] == {"base": "invalid_auth"} @@ -90,7 +90,7 @@ async def test_form_cannot_connect(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["type"] == data_entry_flow.FlowResultType.FORM assert form_result["errors"] == {"base": "cannot_connect"} @@ -112,7 +112,7 @@ async def test_form_unknown_error(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["type"] == data_entry_flow.FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} @@ -148,5 +148,5 @@ async def test_form_entry_exists(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert form_result["type"] == data_entry_flow.FlowResultType.ABORT assert form_result["reason"] == "already_configured" diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 9c510bb3db0..126e61cf629 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -273,7 +273,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -281,7 +281,7 @@ async def test_options_flow(hass): user_input={CONF_FILTER: ["0"], CONF_OFFSET: 15, CONF_REAL_TIME: False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_FILTER: [ { @@ -329,7 +329,7 @@ async def test_options_flow_invalid_auth(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_auth"} @@ -364,7 +364,7 @@ async def test_options_flow_cannot_connect(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 5d2e77e8adb..27f6f25856d 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -166,7 +166,7 @@ async def test_user_if_no_configuration(hass: HomeAssistant) -> None: """Check flow behavior when no configuration is present.""" result = await _init_flow(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["handler"] == DOMAIN @@ -181,7 +181,7 @@ async def test_user_existing_id_abort(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -197,7 +197,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Fail the auth check call. @@ -207,7 +207,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -226,7 +226,7 @@ async def test_user_confirm_cannot_connect(hass: HomeAssistant) -> None: side_effect=[good_client, bad_client], ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -242,7 +242,7 @@ async def test_user_confirm_id_error(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_id" @@ -256,7 +256,7 @@ async def test_user_noauth_flow_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -275,7 +275,7 @@ async def test_user_auth_required(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -289,7 +289,7 @@ async def test_auth_static_token_auth_required_fail(hass: HomeAssistant) -> None "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -309,7 +309,7 @@ async def test_auth_static_token_success(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -335,7 +335,7 @@ async def test_auth_static_token_login_connect_fail(hass: HomeAssistant) -> None hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -358,7 +358,7 @@ async def test_auth_static_token_login_fail(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_access_token" @@ -373,7 +373,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -387,7 +387,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, @@ -395,13 +395,13 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_granted_error" @@ -479,7 +479,7 @@ async def test_auth_create_token_when_issued_token_fails( "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -492,14 +492,14 @@ async def test_auth_create_token_when_issued_token_fails( result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. @@ -508,7 +508,7 @@ async def test_auth_create_token_when_issued_token_fails( client.async_client_connect = AsyncMock(return_value=False) result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -523,7 +523,7 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -536,19 +536,19 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -594,7 +594,7 @@ async def test_auth_create_token_success_but_login_fail( # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_work_error" @@ -614,7 +614,7 @@ async def test_ssdp_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -635,7 +635,7 @@ async def test_ssdp_cannot_connect(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -653,7 +653,7 @@ async def test_ssdp_missing_serial(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_id" @@ -672,7 +672,7 @@ async def test_ssdp_failure_bad_port_json(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_PORT] == const.DEFAULT_PORT_JSON @@ -693,7 +693,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: ): result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -702,7 +702,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" # Verify a working URL is used despite the bad port number @@ -726,8 +726,8 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result_1["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result_2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_1["type"] == data_entry_flow.FlowResultType.FORM + assert result_2["type"] == data_entry_flow.FlowResultType.ABORT assert result_2["reason"] == "already_in_progress" @@ -745,7 +745,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: assert hass.states.get(TEST_ENTITY_ID_1) is not None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" new_priority = 1 @@ -754,7 +754,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: user_input={CONF_PRIORITY: new_priority}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_PRIORITY] == new_priority # Turn the light on and ensure the new priority is used. @@ -788,14 +788,14 @@ async def test_options_effect_show_list(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_EFFECT_SHOW_LIST: ["effect1", "effect3"]}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # effect1 and effect3 only, so effect2 & external sources are hidden. assert result["data"][CONF_EFFECT_HIDE_LIST] == sorted( @@ -818,7 +818,7 @@ async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistant) -> N client.async_client_connect = AsyncMock(return_value=False) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -845,13 +845,13 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert CONF_TOKEN in config_entry.data @@ -877,5 +877,5 @@ async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/ialarm/test_config_flow.py b/tests/components/ialarm/test_config_flow.py index 102b2edb0a6..f7c0a4ed338 100644 --- a/tests/components/ialarm/test_config_flow.py +++ b/tests/components/ialarm/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DATA["host"] assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -56,7 +56,7 @@ async def test_form_cannot_connect(hass): result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -74,7 +74,7 @@ async def test_form_exception(hass): result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -100,5 +100,5 @@ async def test_form_already_exists(hass): result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index cd72aae0eff..3c854d468b8 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -162,7 +162,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with required @@ -171,7 +171,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -187,7 +187,7 @@ async def test_user_with_cookie(hass: HomeAssistant, service_authenticated: Magi CONF_WITH_FAMILY: WITH_FAMILY, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -208,7 +208,7 @@ async def test_login_failed(hass: HomeAssistant): context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -221,7 +221,7 @@ async def test_no_device( context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_device" @@ -234,7 +234,7 @@ async def test_trusted_device(hass: HomeAssistant, service: MagicMock): ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -249,7 +249,7 @@ async def test_trusted_device_success(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -266,7 +266,7 @@ async def test_send_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {CONF_TRUSTED_DEVICE: "send_verification_code"} @@ -283,7 +283,7 @@ async def test_verification_code(hass: HomeAssistant, service: MagicMock): ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -302,7 +302,7 @@ async def test_verification_code_success(hass: HomeAssistant, service: MagicMock result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -328,7 +328,7 @@ async def test_validate_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {"base": "validate_verification_code"} @@ -347,7 +347,7 @@ async def test_2fa_code_success(hass: HomeAssistant, service_2fa: MagicMock): result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -371,7 +371,7 @@ async def test_validate_2fa_code_failed( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE assert result["errors"] == {"base": "validate_verification_code"} @@ -389,13 +389,13 @@ async def test_password_update(hass: HomeAssistant, service_authenticated: Magic data={**MOCK_CONFIG, "unique_id": USERNAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_PASSWORD] == PASSWORD_2 @@ -413,7 +413,7 @@ async def test_password_update_wrong_password(hass: HomeAssistant): data={**MOCK_CONFIG, "unique_id": USERNAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.icloud.config_flow.PyiCloudService.authenticate", @@ -423,5 +423,5 @@ async def test_password_update_wrong_password(hass: HomeAssistant): result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index 1ca8395e11f..59bf51de9bd 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -15,10 +15,10 @@ async def test_config_flow_registers_webhook(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "ifttt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] ifttt_events = [] diff --git a/tests/components/inkbird/__init__.py b/tests/components/inkbird/__init__.py new file mode 100644 index 00000000000..0a74e50ae0e --- /dev/null +++ b/tests/components/inkbird/__init__.py @@ -0,0 +1,36 @@ +"""Tests for the INKBIRD integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_INKBIRD_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +SPS_SERVICE_INFO = BluetoothServiceInfo( + name="sps", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + service_data={}, + manufacturer_data={2096: b"\x0f\x12\x00Z\xc7W\x06"}, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + source="local", +) + +IBBQ_SERVICE_INFO = BluetoothServiceInfo( + name="iBBQ", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={ + 0: b"\x00\x000\xe2\x83}\xb5\x02\xc8\x00\xc8\x00\xc8\x00\xc8\x00" + }, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) diff --git a/tests/components/inkbird/conftest.py b/tests/components/inkbird/conftest.py new file mode 100644 index 00000000000..3450cb933fe --- /dev/null +++ b/tests/components/inkbird/conftest.py @@ -0,0 +1,8 @@ +"""INKBIRD session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py new file mode 100644 index 00000000000..c1f8b3ef545 --- /dev/null +++ b/tests/components/inkbird/test_config_flow.py @@ -0,0 +1,164 @@ +"""Test the INKBIRD config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.inkbird.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import IBBQ_SERVICE_INFO, NOT_INKBIRD_SERVICE_INFO, SPS_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=IBBQ_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "iBBQ 6AADDD4CAC3D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_bluetooth_not_inkbird(hass): + """Test discovery via bluetooth not inkbird.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_INKBIRD_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py new file mode 100644 index 00000000000..cafc22911c3 --- /dev/null +++ b/tests/components/inkbird/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the INKBIRD config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.inkbird.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import SPS_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(SPS_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.ibs_th_75bbe1738105_battery") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "87" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH 75BBE1738105 Battery" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index ef64b1e0969..417769d6696 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from pyinsteon.address import Address from pyinsteon.constants import ALDBStatus, ResponseStatus from pyinsteon.device_types import ( + AccessControl_Morningstar, DimmableLightingControl_KeypadLinc_8, GeneralController_RemoteLinc, Hub, @@ -59,12 +60,13 @@ class MockDevices: async def async_load(self, *args, **kwargs): """Load the mock devices.""" - if self._connected: + if self._connected and not self._devices: addr0 = Address("AA.AA.AA") addr1 = Address("11.11.11") addr2 = Address("22.22.22") addr3 = Address("33.33.33") addr4 = Address("44.44.44") + addr5 = Address("55.55.55") self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0") self._devices[addr1] = MockSwitchLinc( addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1" @@ -78,9 +80,12 @@ class MockDevices: self._devices[addr4] = SensorsActuators_IOLink( addr4, 0x07, 0x00, 0x00, "Device 44.44.44", "4" ) + self._devices[addr5] = AccessControl_Morningstar( + addr5, 0x0F, 0x0A, 0x00, "Device 55.55.55", "5" + ) for device in [ - self._devices[addr] for addr in [addr1, addr2, addr3, addr4] + self._devices[addr] for addr in [addr1, addr2, addr3, addr4, addr5] ]: device.async_read_config = AsyncMock() device.aldb.async_write = AsyncMock() @@ -99,7 +104,9 @@ class MockDevices: return_value=ResponseStatus.SUCCESS ) - for device in [self._devices[addr] for addr in [addr2, addr3, addr4]]: + for device in [ + self._devices[addr] for addr in [addr2, addr3, addr4, addr5] + ]: device.async_status = AsyncMock() self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) self._devices[addr0].aldb.async_load = AsyncMock() @@ -117,6 +124,12 @@ class MockDevices: return_value=ResponseStatus.FAILURE ) + self._devices[addr5].async_lock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) + self._devices[addr5].async_unlock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) self.modem = self._devices[addr0] self.modem.async_read_config = AsyncMock() @@ -155,6 +168,6 @@ class MockDevices: yield address await asyncio.sleep(0.01) - def subscribe(self, listener): + def subscribe(self, listener, force_strong_ref=False): """Mock the subscribe function.""" subscribe_topic(listener, DEVICE_LIST_CHANGED) diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 878b540b721..35a32ef969c 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -71,7 +71,7 @@ async def _init_form(hass, modem_type): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -118,7 +118,7 @@ async def test_fail_on_existing(hass: HomeAssistant): data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -223,7 +223,7 @@ async def _options_init_form(hass, entry_id, step): with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): result = await hass.config_entries.options.async_init(entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -279,7 +279,7 @@ async def test_import_existing(hass: HomeAssistant): result = await _import_config( hass, {**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -295,7 +295,7 @@ async def test_import_failed_connection(hass: HomeAssistant): data={**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -329,7 +329,7 @@ async def test_options_change_hub_config(hass: HomeAssistant): } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {} assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} @@ -353,7 +353,7 @@ async def test_options_add_device_override(hass: HomeAssistant): } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 assert config_entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" assert config_entry.options[CONF_OVERRIDE][0][CONF_CAT] == 4 @@ -397,7 +397,7 @@ async def test_options_remove_device_override(hass: HomeAssistant): user_input = {CONF_ADDRESS: "1A.2B.3C"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 @@ -429,7 +429,7 @@ async def test_options_remove_device_override_with_x10(hass: HomeAssistant): user_input = {CONF_ADDRESS: "1A.2B.3C"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 assert len(config_entry.options[CONF_X10]) == 1 @@ -454,7 +454,7 @@ async def test_options_add_x10_device(hass: HomeAssistant): } result2, _ = await _options_form(hass, result["flow_id"], user_input) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 assert config_entry.options[CONF_X10][0][CONF_HOUSECODE] == "c" assert config_entry.options[CONF_X10][0][CONF_UNITCODE] == 12 @@ -470,7 +470,7 @@ async def test_options_add_x10_device(hass: HomeAssistant): } result3, _ = await _options_form(hass, result["flow_id"], user_input) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 2 assert config_entry.options[CONF_X10][1][CONF_HOUSECODE] == "d" assert config_entry.options[CONF_X10][1][CONF_UNITCODE] == 10 @@ -511,7 +511,7 @@ async def test_options_remove_x10_device(hass: HomeAssistant): user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 @@ -546,7 +546,7 @@ async def test_options_remove_x10_device_with_override(hass: HomeAssistant): user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 assert len(config_entry.options[CONF_OVERRIDE]) == 1 @@ -562,14 +562,14 @@ async def test_options_dup_selection(hass: HomeAssistant): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( result["flow_id"], {STEP_ADD_OVERRIDE: True, STEP_ADD_X10: True}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "select_single"} @@ -593,7 +593,7 @@ async def test_options_override_bad_data(hass: HomeAssistant): } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "input_error"} @@ -611,7 +611,7 @@ async def test_discovery_via_usb(hass): "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm_usb" with patch("homeassistant.components.insteon.config_flow.async_connect"), patch( @@ -622,7 +622,7 @@ async def test_discovery_via_usb(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == {"device": "/dev/ttyINSTEON"} @@ -646,5 +646,5 @@ async def test_discovery_via_usb_already_setup(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/insteon/test_lock.py b/tests/components/insteon/test_lock.py new file mode 100644 index 00000000000..6f847543a9f --- /dev/null +++ b/tests/components/insteon/test_lock.py @@ -0,0 +1,109 @@ +"""Tests for the Insteon lock.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components import insteon +from homeassistant.components.insteon import ( + DOMAIN, + insteon_entity, + utils as insteon_utils, +) +from homeassistant.components.lock import ( # SERVICE_LOCK,; SERVICE_UNLOCK, + DOMAIN as LOCK_DOMAIN, +) +from homeassistant.const import ( # ATTR_ENTITY_ID,; + EVENT_HOMEASSISTANT_STOP, + STATE_LOCKED, + STATE_UNLOCKED, + Platform, +) +from homeassistant.helpers import entity_registry as er + +from .const import MOCK_USER_INPUT_PLM +from .mock_devices import MockDevices + +from tests.common import MockConfigEntry + +devices = MockDevices() + + +@pytest.fixture(autouse=True) +def lock_platform_only(): + """Only setup the lock and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.insteon.INSTEON_PLATFORMS", + (Platform.LOCK,), + ): + yield + + +@pytest.fixture(autouse=True) +def patch_setup_and_devices(): + """Patch the Insteon setup process and devices.""" + with patch.object(insteon, "async_connect", new=mock_connection), patch.object( + insteon, "async_close" + ), patch.object(insteon, "devices", devices), patch.object( + insteon_utils, "devices", devices + ), patch.object( + insteon_entity, "devices", devices + ): + yield + + +async def mock_connection(*args, **kwargs): + """Return a successful connection.""" + return True + + +async def test_lock_lock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + assert state.state is STATE_UNLOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "lock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_lock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_lock_unlock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + devices["55.55.55"].groups[1].set_value(255) + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + + assert state.state is STATE_LOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "unlock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_unlock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py index 5992d480f80..9ad15096ad3 100644 --- a/tests/components/integration/test_config_flow.py +++ b/tests/components/integration/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.integration.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My integration" assert result["data"] == {} assert result["options"] == { @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -109,7 +109,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "round": 2.0, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "method": "left", "name": "My integration", diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 06fcbea5bfa..95e5d735c35 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING from homeassistant.components.intellifire.const import CONF_USER_ID, DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.intellifire.conftest import mock_api_connection_error @@ -38,7 +34,7 @@ async def test_no_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_device_entry" @@ -50,7 +46,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -59,7 +55,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Fireplace 12345" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -100,7 +96,7 @@ async def test_single_discovery( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["errors"] == {"base": "iftapi_connect"} @@ -133,7 +129,7 @@ async def test_single_discovery_loign_error( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["errors"] == {"base": "api_error"} @@ -199,14 +195,14 @@ async def test_multi_discovery_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_device" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -221,7 +217,7 @@ async def test_form_cannot_connect_manual_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_device_entry" result2 = await hass.config_entries.flow.async_configure( @@ -231,7 +227,7 @@ async def test_form_cannot_connect_manual_entry( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -266,7 +262,7 @@ async def test_picker_already_discovered( CONF_HOST: "192.168.1.4", }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert len(mock_setup_entry.mock_calls) == 0 @@ -303,7 +299,7 @@ async def test_reauth_flow( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -311,7 +307,7 @@ async def test_reauth_flow( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert entry.data[CONF_PASSWORD] == "AROONIE" assert entry.data[CONF_USERNAME] == "test" @@ -331,10 +327,10 @@ async def test_dhcp_discovery_intellifire_device( hostname="zentrios-Test", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "dhcp_confirm" result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "dhcp_confirm" result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={} diff --git a/tests/components/iotawatt/test_config_flow.py b/tests/components/iotawatt/test_config_flow.py index 6adb3534b15..06c9cce0da9 100644 --- a/tests/components/iotawatt/test_config_flow.py +++ b/tests/components/iotawatt/test_config_flow.py @@ -6,7 +6,7 @@ import httpx from homeassistant import config_entries from homeassistant.components.iotawatt.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -15,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == { "host": "1.1.1.1", } @@ -46,7 +46,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -59,7 +59,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "auth" with patch( @@ -75,7 +75,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["step_id"] == "auth" assert result3["errors"] == {"base": "invalid_auth"} @@ -95,7 +95,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 assert result4["data"] == { "host": "1.1.1.1", @@ -119,7 +119,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -138,5 +138,5 @@ async def test_form_setup_exception(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 8eefa4251f7..a37fdc9de52 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_USER_INPUT, @@ -31,7 +27,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_show_zeroconf_form( @@ -48,7 +44,7 @@ async def test_show_zeroconf_form( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} @@ -66,7 +62,7 @@ async def test_connection_error( ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -83,7 +79,7 @@ async def test_zeroconf_connection_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -98,7 +94,7 @@ async def test_zeroconf_confirm_connection_error( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -116,7 +112,7 @@ async def test_user_connection_upgrade_required( ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "connection_upgrade"} @@ -133,7 +129,7 @@ async def test_zeroconf_connection_upgrade_required( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "connection_upgrade" @@ -150,7 +146,7 @@ async def test_user_parse_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -167,7 +163,7 @@ async def test_zeroconf_parse_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -184,7 +180,7 @@ async def test_user_ipp_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -201,7 +197,7 @@ async def test_zeroconf_ipp_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -218,7 +214,7 @@ async def test_user_ipp_version_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -235,7 +231,7 @@ async def test_zeroconf_ipp_version_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -252,7 +248,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -269,7 +265,7 @@ async def test_zeroconf_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -291,7 +287,7 @@ async def test_zeroconf_with_uuid_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -312,7 +308,7 @@ async def test_zeroconf_empty_unique_id( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_zeroconf_no_unique_id( @@ -328,7 +324,7 @@ async def test_zeroconf_no_unique_id( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_full_user_flow_implementation( @@ -343,7 +339,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -351,7 +347,7 @@ async def test_full_user_flow_implementation( user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.31" assert result["data"] @@ -376,14 +372,14 @@ async def test_full_zeroconf_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -410,7 +406,7 @@ async def test_full_zeroconf_tls_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): @@ -418,7 +414,7 @@ async def test_full_zeroconf_tls_flow_implementation( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 315098a44d8..0d881c30a35 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -9,7 +9,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -18,7 +18,7 @@ async def test_invalid_zip_code(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_ZIP_CODE: "bad"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_ZIP_CODE: "invalid_zip_code"} @@ -27,7 +27,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -36,6 +36,6 @@ async def test_step_user(hass, config, setup_iqvia): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == {CONF_ZIP_CODE: "12345"} diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 730c5634770..d447da1c61e 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -26,13 +26,13 @@ async def test_flow_works(hass): result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Islamic Prayer Times" @@ -48,14 +48,14 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_CALC_METHOD: "makkah"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_CALC_METHOD] == "makkah" @@ -71,5 +71,5 @@ async def test_integration_already_configured(hass): islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index 3260b76432f..eabca610ddf 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -18,7 +18,7 @@ async def test_create_entry(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == SOURCE_USER with patch("homeassistant.components.iss.async_setup_entry", return_value=True): @@ -28,7 +28,7 @@ async def test_create_entry(hass: HomeAssistant): {}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -44,7 +44,7 @@ async def test_integration_already_exists(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -60,7 +60,7 @@ async def test_abort_no_home(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "latitude_longitude_not_defined" diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 8458dc0dc67..b87662718e5 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -116,7 +116,7 @@ async def test_form(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( @@ -130,7 +130,7 @@ async def test_form(hass: HomeAssistant): MOCK_USER_INPUT, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -154,7 +154,7 @@ async def test_form_invalid_host(hass: HomeAssistant): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_host"} @@ -172,7 +172,7 @@ async def test_form_invalid_auth(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -190,7 +190,7 @@ async def test_form_unknown_exeption(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -208,7 +208,7 @@ async def test_form_isy_connection_error(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -226,7 +226,7 @@ async def test_form_isy_parse_response_error(hass: HomeAssistant, caplog): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert "ISY Could not parse response, poorly formatted XML." in caplog.text @@ -246,7 +246,7 @@ async def test_form_no_name_in_response(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -257,7 +257,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE): @@ -265,7 +265,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant): result["flow_id"], MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT async def test_import_flow_some_fields(hass: HomeAssistant) -> None: @@ -282,7 +282,7 @@ async def test_import_flow_some_fields(hass: HomeAssistant) -> None: data=MOCK_IMPORT_BASIC_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}" assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD @@ -303,7 +303,7 @@ async def test_import_flow_with_https(hass: HomeAssistant) -> None: data=MOCK_IMPORT_WITH_SSL, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == f"https://{MOCK_HOSTNAME}" assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD @@ -323,7 +323,7 @@ async def test_import_flow_all_fields(hass: HomeAssistant) -> None: data=MOCK_IMPORT_FULL_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}" assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD @@ -356,7 +356,7 @@ async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_form_ssdp(hass: HomeAssistant): @@ -375,7 +375,7 @@ async def test_form_ssdp(hass: HomeAssistant): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -391,7 +391,7 @@ async def test_form_ssdp(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -425,7 +425,7 @@ async def test_form_ssdp_existing_entry(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80{ISY_URL_POSTFIX}" @@ -456,7 +456,7 @@ async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80/{ISY_URL_POSTFIX}" @@ -487,7 +487,7 @@ async def test_form_ssdp_existing_entry_with_alternate_port(hass: HomeAssistant) ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}" @@ -518,7 +518,7 @@ async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"https://3.3.3.3:443/{ISY_URL_POSTFIX}" @@ -535,7 +535,7 @@ async def test_form_dhcp(hass: HomeAssistant): macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -551,7 +551,7 @@ async def test_form_dhcp(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -571,7 +571,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant): macaddress=MOCK_POLISY_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} assert ( @@ -591,7 +591,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_POLISY_USER_INPUT @@ -621,7 +621,7 @@ async def test_form_dhcp_existing_entry(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4{ISY_URL_POSTFIX}" @@ -651,7 +651,7 @@ async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4:1443{ISY_URL_POSTFIX}" assert entry.data[CONF_USERNAME] == "bob" @@ -677,7 +677,7 @@ async def test_form_dhcp_existing_ignored_entry(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index a548e59930b..d48e19181c8 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -42,10 +42,10 @@ async def test_not_found(hass, mock_disco): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() @@ -71,10 +71,10 @@ async def test_found(hass, mock_disco): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index cc23265e011..e898f8ac5ce 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -27,7 +27,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/kaleidescape/test_config_flow.py b/tests/components/kaleidescape/test_config_flow.py index a2cf8091d02..8171ed0955b 100644 --- a/tests/components/kaleidescape/test_config_flow.py +++ b/tests/components/kaleidescape/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.kaleidescape.const import DOMAIN from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_HOST, MOCK_SSDP_DISCOVERY_INFO @@ -25,7 +21,7 @@ async def test_user_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -33,7 +29,7 @@ async def test_user_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -48,7 +44,7 @@ async def test_user_config_flow_bad_connect_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -63,7 +59,7 @@ async def test_user_config_flow_unsupported_device_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unsupported"} @@ -75,7 +71,7 @@ async def test_user_config_flow_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -87,7 +83,7 @@ async def test_ssdp_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( @@ -95,7 +91,7 @@ async def test_ssdp_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -111,7 +107,7 @@ async def test_ssdp_config_flow_bad_connect_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -126,5 +122,5 @@ async def test_ssdp_config_flow_unsupported_device_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unsupported" diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 18d5f3df1fb..e1cb083dc73 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -51,7 +51,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -63,7 +63,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.options.async_configure( @@ -106,7 +106,7 @@ async def test_options(hass: HomeAssistant) -> None: user_input=MOCK_OPTIONS, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == MOCK_OPTIONS @@ -126,7 +126,7 @@ async def test_host_already_configured(hass: HomeAssistant, connect) -> None: result["flow_id"], user_input=MOCK_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -139,7 +139,7 @@ async def test_connection_error(hass: HomeAssistant, connect_error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -153,7 +153,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -168,7 +168,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -189,7 +189,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -210,7 +210,7 @@ async def test_ssdp_ignored(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -236,7 +236,7 @@ async def test_ssdp_update_host(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == new_ip @@ -254,7 +254,7 @@ async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_udn" @@ -270,5 +270,5 @@ async def test_ssdp_reject_non_keenetic(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_keenetic_ndms2" diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index fabd8738259..c2965dfa083 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -69,14 +69,14 @@ async def test_form_options(hass, aioclient_mock): assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_REVERSE: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_REVERSE: True} await hass.async_block_till_done() diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index ccfd3a35085..e5cf18b0c3c 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -55,19 +55,23 @@ class KNXTestKit: async def setup_integration(self, config): """Create the KNX integration.""" + def disable_rate_limiter(): + """Disable rate limiter for tests.""" + # after XKNX.__init__() to not overwrite it by the config entry again + # before StateUpdater starts to avoid slow down of tests + self.xknx.rate_limit = 0 + def knx_ip_interface_mock(): """Create a xknx knx ip interface mock.""" mock = Mock() - mock.start = AsyncMock() + mock.start = AsyncMock(side_effect=disable_rate_limiter) mock.stop = AsyncMock() mock.send_telegram = AsyncMock(side_effect=self._outgoing_telegrams.put) return mock def fish_xknx(*args, **kwargs): """Get the XKNX object from the constructor call.""" - self.xknx = kwargs["xknx"] - # disable rate limiter for tests (before StateUpdater starts) - self.xknx.rate_limit = 0 + self.xknx = args[0] return DEFAULT with patch( diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 8d5d57567dc..30b8aa537a6 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -41,11 +41,7 @@ from homeassistant.components.knx.const import ( ) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, - RESULT_TYPE_MENU, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -84,7 +80,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -94,7 +90,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" assert not result2["errors"] @@ -111,7 +107,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == CONF_KNX_ROUTING.capitalize() assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -136,7 +132,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: "show_advanced_options": True, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -146,7 +142,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" assert not result2["errors"] @@ -161,7 +157,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result_invalid_input["type"] == RESULT_TYPE_FORM + assert result_invalid_input["type"] == FlowResultType.FORM assert result_invalid_input["step_id"] == "routing" assert result_invalid_input["errors"] == { CONF_KNX_MCAST_GRP: "invalid_ip_address", @@ -184,7 +180,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == CONF_KNX_ROUTING.capitalize() assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -261,7 +257,7 @@ async def test_tunneling_setup( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -271,7 +267,7 @@ async def test_tunneling_setup( }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert not result2["errors"] @@ -284,7 +280,7 @@ async def test_tunneling_setup( user_input, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Tunneling @ 192.168.0.1" assert result3["data"] == config_entry_data @@ -303,7 +299,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: "show_advanced_options": True, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -313,7 +309,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert not result2["errors"] @@ -328,7 +324,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result_invalid_host["type"] == RESULT_TYPE_FORM + assert result_invalid_host["type"] == FlowResultType.FORM assert result_invalid_host["step_id"] == "manual_tunnel" assert result_invalid_host["errors"] == {CONF_HOST: "invalid_ip_address"} # invalid local ip address @@ -342,7 +338,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result_invalid_local["type"] == RESULT_TYPE_FORM + assert result_invalid_local["type"] == FlowResultType.FORM assert result_invalid_local["step_id"] == "manual_tunnel" assert result_invalid_local["errors"] == {CONF_KNX_LOCAL_IP: "invalid_ip_address"} @@ -361,7 +357,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Tunneling @ 192.168.0.2" assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -385,7 +381,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -395,7 +391,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) }, ) await hass.async_block_till_done() - assert tunnel_flow["type"] == RESULT_TYPE_FORM + assert tunnel_flow["type"] == FlowResultType.FORM assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] @@ -404,7 +400,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) {CONF_KNX_GATEWAY: str(gateway)}, ) await hass.async_block_till_done() - assert manual_tunnel["type"] == RESULT_TYPE_FORM + assert manual_tunnel["type"] == FlowResultType.FORM assert manual_tunnel["step_id"] == "manual_tunnel" with patch( @@ -420,7 +416,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) }, ) await hass.async_block_till_done() - assert manual_tunnel_flow["type"] == RESULT_TYPE_CREATE_ENTRY + assert manual_tunnel_flow["type"] == FlowResultType.CREATE_ENTRY assert manual_tunnel_flow["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, @@ -441,7 +437,7 @@ async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -451,7 +447,7 @@ async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert tunnel_flow["type"] == RESULT_TYPE_FORM + assert tunnel_flow["type"] == FlowResultType.FORM assert tunnel_flow["step_id"] == "manual_tunnel" assert not tunnel_flow["errors"] @@ -463,7 +459,7 @@ async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch( @@ -478,7 +474,7 @@ async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> N ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() assert result2["data"] == { **DEFAULT_ENTRY_DATA, @@ -496,7 +492,7 @@ async def _get_menu_step(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -506,7 +502,7 @@ async def _get_menu_step(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert not result2["errors"] @@ -519,7 +515,7 @@ async def _get_menu_step(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_MENU + assert result3["type"] == FlowResultType.MENU assert result3["step_id"] == "secure_tunneling" return result3 @@ -532,7 +528,7 @@ async def test_configure_secure_manual(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_manual"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_manual" assert not result["errors"] @@ -549,7 +545,7 @@ async def test_configure_secure_manual(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_manual["type"] == RESULT_TYPE_CREATE_ENTRY + assert secure_manual["type"] == FlowResultType.CREATE_ENTRY assert secure_manual["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -574,7 +570,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -592,7 +588,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == RESULT_TYPE_CREATE_ENTRY + assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY assert secure_knxkeys["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -616,7 +612,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -632,7 +628,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == RESULT_TYPE_FORM + assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_FILENAME] == "file_not_found" @@ -645,7 +641,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -661,7 +657,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == RESULT_TYPE_FORM + assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] == "invalid_signature" @@ -679,7 +675,7 @@ async def test_options_flow( mock_config_entry.entry_id ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -694,7 +690,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == { @@ -787,7 +783,7 @@ async def test_tunneling_options_flow( mock_config_entry.entry_id ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -801,7 +797,7 @@ async def test_tunneling_options_flow( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert not result2.get("data") assert "flow_id" in result2 @@ -811,7 +807,7 @@ async def test_tunneling_options_flow( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert not result3.get("data") assert mock_config_entry.data == config_entry_data @@ -880,7 +876,7 @@ async def test_advanced_options( mock_config_entry.entry_id, context={"show_advanced_options": True} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -890,7 +886,7 @@ async def test_advanced_options( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == config_entry_data diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index f0fb42f6e78..f6978612cff 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -1,72 +1,98 @@ """Test Kostal Plenticore number.""" -from unittest.mock import AsyncMock, MagicMock +from collections.abc import Generator +from datetime import timedelta +from unittest.mock import patch -from kostal.plenticore import SettingsData +from kostal.plenticore import PlenticoreApiClient, SettingsData import pytest -from homeassistant.components.kostal_plenticore.const import ( - PlenticoreNumberEntityDescription, +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, ) -from homeassistant.components.kostal_plenticore.number import PlenticoreDataNumber +from homeassistant.components.number.const import ATTR_MAX, ATTR_MIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture -def mock_coordinator() -> MagicMock: - """Return a mocked coordinator for tests.""" - coordinator = MagicMock() - coordinator.async_write_data = AsyncMock() - coordinator.async_refresh = AsyncMock() - return coordinator +def mock_plenticore_client() -> Generator[PlenticoreApiClient, None, None]: + """Return a patched PlenticoreApiClient.""" + with patch( + "homeassistant.components.kostal_plenticore.helper.PlenticoreApiClient", + autospec=True, + ) as plenticore_client_class: + yield plenticore_client_class.return_value @pytest.fixture -def mock_number_description() -> PlenticoreNumberEntityDescription: - """Return a PlenticoreNumberEntityDescription for tests.""" - return PlenticoreNumberEntityDescription( - key="mock key", - module_id="moduleid", - data_id="dataid", - native_min_value=0, - native_max_value=1000, - fmt_from="format_round", - fmt_to="format_round_back", - ) +def mock_get_setting_values(mock_plenticore_client: PlenticoreApiClient) -> list: + """Add a setting value to the given Plenticore client. + Returns a list with setting values which can be extended by test cases. + """ -@pytest.fixture -def mock_setting_data() -> SettingsData: - """Return a default SettingsData for tests.""" - return SettingsData( - { - "default": None, - "min": None, - "access": None, - "max": None, - "unit": None, - "type": None, - "id": "data_id", - } - ) - - -async def test_setup_all_entries( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore: MagicMock -): - """Test if all available entries are setup up.""" - mock_plenticore.client.get_settings.return_value = { + mock_plenticore_client.get_settings.return_value = { "devices:local": [ - SettingsData({"id": "Battery:MinSoc", "min": None, "max": None}), SettingsData( - {"id": "Battery:MinHomeComsumption", "min": None, "max": None} + { + "default": None, + "min": 5, + "max": 100, + "access": "readwrite", + "unit": "%", + "type": "byte", + "id": "Battery:MinSoc", + } + ), + SettingsData( + { + "default": None, + "min": 50, + "max": 38000, + "access": "readwrite", + "unit": "W", + "type": "byte", + "id": "Battery:MinHomeComsumption", + } ), ] } + # this values are always retrieved by the integration on startup + setting_values = [ + { + "devices:local": { + "Properties:SerialNo": "42", + "Branding:ProductName1": "PLENTICORE", + "Branding:ProductName2": "plus 10", + "Properties:VersionIOC": "01.45", + "Properties:VersionMC": " 01.46", + }, + "scb:network": {"Hostname": "scb"}, + } + ] + + mock_plenticore_client.get_setting_values.side_effect = setting_values + + return setting_values + + +async def test_setup_all_entries( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, +): + """Test if all available entries are setup.""" + mock_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -78,10 +104,16 @@ async def test_setup_all_entries( async def test_setup_no_entries( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore: MagicMock + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test that no entries are setup up.""" - mock_plenticore.client.get_settings.return_value = [] + """Test that no entries are setup if Plenticore does not provide data.""" + + mock_plenticore_client.get_settings.return_value = [] + mock_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -92,106 +124,81 @@ async def test_setup_no_entries( assert ent_reg.async_get("number.scb_battery_min_home_consumption") is None -def test_number_returns_value_if_available( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, +async def test_number_has_value( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test if value property on PlenticoreDataNumber returns an int if available.""" + """Test if number has a value if data is provided on update.""" - mock_coordinator.data = {"moduleid": {"dataid": "42"}} + mock_get_setting_values.append({"devices:local": {"Battery:MinSoc": "42"}}) - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) + mock_config_entry.add_to_hass(hass) - assert entity.value == 42 - assert type(entity.value) == int + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + + state = hass.states.get("number.scb_battery_min_soc") + assert state.state == "42" + assert state.attributes[ATTR_MIN] == 5 + assert state.attributes[ATTR_MAX] == 100 -def test_number_returns_none_if_unavailable( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, +async def test_number_is_unavailable( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test if value property on PlenticoreDataNumber returns none if unavailable.""" + """Test if number is unavailable if no data is provided on update.""" - mock_coordinator.data = {} # makes entity not available + mock_config_entry.add_to_hass(hass) - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() - assert entity.value is None + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + + state = hass.states.get("number.scb_battery_min_soc") + assert state.state == STATE_UNAVAILABLE async def test_set_value( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test if set value calls coordinator with new value.""" + """Test if a new value could be set.""" - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) + mock_get_setting_values.append({"devices:local": {"Battery:MinSoc": "42"}}) - await entity.async_set_native_value(42) + mock_config_entry.add_to_hass(hass) - mock_coordinator.async_write_data.assert_called_once_with( - "moduleid", {"dataid": "42"} - ) - mock_coordinator.async_refresh.assert_called_once() + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() -async def test_minmax_overwrite( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, -): - """Test if min/max value is overwritten from retrieved settings data.""" - - setting_data = SettingsData( + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, { - "min": "5", - "max": "100", - } + ATTR_ENTITY_ID: "number.scb_battery_min_soc", + ATTR_VALUE: 80, + }, + blocking=True, ) - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, setting_data + mock_plenticore_client.set_setting_values.assert_called_once_with( + "devices:local", {"Battery:MinSoc": "80"} ) - - assert entity.min_value == 5 - assert entity.max_value == 100 - - -async def test_added_to_hass( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, -): - """Test if coordinator starts fetching after added to hass.""" - - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) - - await entity.async_added_to_hass() - - mock_coordinator.start_fetch_data.assert_called_once_with("moduleid", "dataid") - - -async def test_remove_from_hass( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, -): - """Test if coordinator stops fetching after remove from hass.""" - - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) - - await entity.async_will_remove_from_hass() - - mock_coordinator.stop_fetch_data.assert_called_once_with("moduleid", "dataid") diff --git a/tests/components/launch_library/test_config_flow.py b/tests/components/launch_library/test_config_flow.py index 5b6cb85cfee..a9dd794d05e 100644 --- a/tests/components/launch_library/test_config_flow.py +++ b/tests/components/launch_library/test_config_flow.py @@ -15,7 +15,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == SOURCE_USER with patch( @@ -27,7 +27,7 @@ async def test_create_entry(hass): {}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -43,5 +43,5 @@ async def test_integration_already_exists(hass): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/laundrify/test_config_flow.py b/tests/components/laundrify/test_config_flow.py index 5ee3efe1e45..d9715c2d025 100644 --- a/tests/components/laundrify/test_config_flow.py +++ b/tests/components/laundrify/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components.laundrify.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CODE, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import create_entry from .const import VALID_ACCESS_TOKEN, VALID_AUTH_CODE, VALID_USER_INPUT @@ -21,7 +17,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_ACCESS_TOKEN: VALID_ACCESS_TOKEN, @@ -50,7 +46,7 @@ async def test_form_invalid_format( data={CONF_CODE: "invalidFormat"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_format"} @@ -63,7 +59,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, laundrify_exchange_code) - data=VALID_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_auth"} @@ -76,7 +72,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, laundrify_exchange_code) data=VALID_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -89,7 +85,7 @@ async def test_form_unkown_exception(hass: HomeAssistant, laundrify_exchange_cod data=VALID_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -99,7 +95,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_REAUTH} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -108,7 +104,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_integration_already_exists(hass: HomeAssistant): @@ -125,5 +121,5 @@ async def test_integration_already_exists(hass: HomeAssistant): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index 36b5d23739a..6f084f939d8 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -43,7 +43,7 @@ async def test_step_import(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "pchk" assert result["data"] == IMPORT_DATA @@ -65,7 +65,7 @@ async def test_step_import_existing_host(hass): await hass.async_block_till_done() # Check if config entry was updated - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "existing_configuration_updated" assert mock_entry.source == config_entries.SOURCE_IMPORT assert mock_entry.data == IMPORT_DATA @@ -91,5 +91,5 @@ async def test_step_import_error(hass, error, reason): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py index 0b5b850ac23..02d5539e117 100644 --- a/tests/components/life360/test_config_flow.py +++ b/tests/components/life360/test_config_flow.py @@ -106,7 +106,7 @@ async def test_user_show_form(hass, life360_api): life360_api.get_authorization.assert_not_called() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -134,7 +134,7 @@ async def test_user_config_flow_success(hass, life360_api): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USER.lower() assert result["data"] == TEST_CONFIG_DATA assert result["options"] == DEFAULT_OPTIONS @@ -159,7 +159,7 @@ async def test_user_config_flow_error(hass, life360_api, caplog, exception, erro life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] assert result["errors"]["base"] == error @@ -190,7 +190,7 @@ async def test_user_config_flow_already_configured(hass, life360_api): life360_api.get_authorization.assert_not_called() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -220,7 +220,7 @@ async def test_reauth_config_flow_success(hass, life360_api, caplog, state): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert "Reauthorization successful" in caplog.text @@ -250,7 +250,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, caplog): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_auth" @@ -274,7 +274,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, caplog): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert "Reauthorization successful" in caplog.text @@ -292,7 +292,7 @@ async def test_options_flow(hass): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert not result["errors"] @@ -303,7 +303,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_configure(flow_id, USER_OPTIONS) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == TEST_OPTIONS assert config_entry.options == TEST_OPTIONS diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py new file mode 100644 index 00000000000..fdea992c87d --- /dev/null +++ b/tests/components/lifx/__init__.py @@ -0,0 +1,217 @@ +"""Tests for the lifx integration.""" +from __future__ import annotations + +import asyncio +from contextlib import contextmanager +from unittest.mock import AsyncMock, MagicMock, patch + +from aiolifx.aiolifx import Light + +from homeassistant.components.lifx import discovery +from homeassistant.components.lifx.const import TARGET_ANY + +MODULE = "homeassistant.components.lifx" +MODULE_CONFIG_FLOW = "homeassistant.components.lifx.config_flow" +IP_ADDRESS = "127.0.0.1" +LABEL = "My Bulb" +SERIAL = "aa:bb:cc:dd:ee:cc" +MAC_ADDRESS = "aa:bb:cc:dd:ee:cd" +DEFAULT_ENTRY_TITLE = LABEL + + +class MockMessage: + """Mock a lifx message.""" + + def __init__(self): + """Init message.""" + self.target_addr = SERIAL + self.count = 9 + + +class MockFailingLifxCommand: + """Mock a lifx command that fails.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.calls = [] + + def __call__(self, *args, **kwargs): + """Call command.""" + if callb := kwargs.get("callb"): + callb(self.bulb, None) + self.calls.append([args, kwargs]) + + def reset_mock(self): + """Reset mock.""" + self.calls = [] + + +class MockLifxCommand: + """Mock a lifx command.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.calls = [] + + def __call__(self, *args, **kwargs): + """Call command.""" + if callb := kwargs.get("callb"): + callb(self.bulb, MockMessage()) + self.calls.append([args, kwargs]) + + def reset_mock(self): + """Reset mock.""" + self.calls = [] + + +def _mocked_bulb() -> Light: + bulb = Light(asyncio.get_running_loop(), SERIAL, IP_ADDRESS) + bulb.host_firmware_version = "3.00" + bulb.label = LABEL + bulb.color = [1, 2, 3, 4] + bulb.power_level = 0 + bulb.try_sending = AsyncMock() + bulb.set_infrared = MockLifxCommand(bulb) + bulb.get_color = MockLifxCommand(bulb) + bulb.set_power = MockLifxCommand(bulb) + bulb.set_color = MockLifxCommand(bulb) + bulb.get_hostfirmware = MockLifxCommand(bulb) + bulb.get_version = MockLifxCommand(bulb) + bulb.product = 1 # LIFX Original 1000 + return bulb + + +def _mocked_failing_bulb() -> Light: + bulb = _mocked_bulb() + bulb.get_color = MockFailingLifxCommand(bulb) + bulb.set_power = MockFailingLifxCommand(bulb) + bulb.set_color = MockFailingLifxCommand(bulb) + bulb.get_hostfirmware = MockFailingLifxCommand(bulb) + bulb.get_version = MockFailingLifxCommand(bulb) + return bulb + + +def _mocked_white_bulb() -> Light: + bulb = _mocked_bulb() + bulb.product = 19 # LIFX White 900 BR30 (High Voltage) + return bulb + + +def _mocked_brightness_bulb() -> Light: + bulb = _mocked_bulb() + bulb.product = 51 # LIFX Mini White + return bulb + + +def _mocked_light_strip() -> Light: + bulb = _mocked_bulb() + bulb.product = 31 # LIFX Z + bulb.get_color_zones = MockLifxCommand(bulb) + bulb.set_color_zones = MockLifxCommand(bulb) + bulb.color_zones = [MagicMock(), MagicMock()] + return bulb + + +def _mocked_bulb_new_firmware() -> Light: + bulb = _mocked_bulb() + bulb.host_firmware_version = "3.90" + return bulb + + +def _mocked_relay() -> Light: + bulb = _mocked_bulb() + bulb.product = 70 # LIFX Switch + return bulb + + +def _patch_device(device: Light | None = None, no_device: bool = False): + """Patch out discovery.""" + + class MockLifxConnecton: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + if no_device: + self.device = _mocked_failing_bulb() + else: + self.device = device or _mocked_bulb() + self.device.mac_addr = TARGET_ANY + + async def async_setup(self): + """Mock setup.""" + + def async_stop(self): + """Mock teardown.""" + + @contextmanager + def _patcher(): + with patch("homeassistant.components.lifx.LIFXConnection", MockLifxConnecton): + yield + + return _patcher() + + +def _patch_discovery(device: Light | None = None, no_device: bool = False): + """Patch out discovery.""" + + class MockLifxDiscovery: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init discovery.""" + if no_device: + self.lights = {} + return + discovered = device or _mocked_bulb() + self.lights = {discovered.mac_addr: discovered} + + def start(self): + """Mock start.""" + + def cleanup(self): + """Mock cleanup.""" + + @contextmanager + def _patcher(): + with patch.object(discovery, "DEFAULT_TIMEOUT", 0), patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + ): + yield + + return _patcher() + + +def _patch_config_flow_try_connect( + device: Light | None = None, no_device: bool = False +): + """Patch out discovery.""" + + class MockLifxConnecton: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + if no_device: + self.device = _mocked_failing_bulb() + else: + self.device = device or _mocked_bulb() + self.device.mac_addr = TARGET_ANY + + async def async_setup(self): + """Mock setup.""" + + def async_stop(self): + """Mock teardown.""" + + @contextmanager + def _patcher(): + with patch( + "homeassistant.components.lifx.config_flow.LIFXConnection", + MockLifxConnecton, + ): + yield + + return _patcher() diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py new file mode 100644 index 00000000000..326c4f75413 --- /dev/null +++ b/tests/components/lifx/conftest.py @@ -0,0 +1,57 @@ +"""Tests for the lifx integration.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from tests.common import mock_device_registry, mock_registry + + +@pytest.fixture +def mock_effect_conductor(): + """Mock the effect conductor.""" + + class MockConductor: + def __init__(self, *args, **kwargs) -> None: + """Mock the conductor.""" + self.start = AsyncMock() + self.stop = AsyncMock() + + def effect(self, bulb): + """Mock effect.""" + return MagicMock() + + mock_conductor = MockConductor() + + with patch( + "homeassistant.components.lifx.manager.aiolifx_effects.Conductor", + return_value=mock_conductor, + ): + yield mock_conductor + + +@pytest.fixture(autouse=True) +def lifx_mock_get_source_ip(mock_get_source_ip): + """Mock network util's async_get_source_ip.""" + + +@pytest.fixture(autouse=True) +def lifx_mock_async_get_ipv4_broadcast_addresses(): + """Mock network util's async_get_ipv4_broadcast_addresses.""" + with patch( + "homeassistant.components.network.async_get_ipv4_broadcast_addresses", + return_value=["255.255.255.255"], + ): + yield + + +@pytest.fixture(name="device_reg") +def device_reg_fixture(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture(name="entity_reg") +def entity_reg_fixture(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py new file mode 100644 index 00000000000..f007e9ee0e8 --- /dev/null +++ b/tests/components/lifx/test_config_flow.py @@ -0,0 +1,508 @@ +"""Tests for the lifx integration config flow.""" +import socket +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components import dhcp, zeroconf +from homeassistant.components.lifx import DOMAIN +from homeassistant.components.lifx.const import CONF_SERIAL +from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + LABEL, + MAC_ADDRESS, + MODULE, + SERIAL, + _mocked_failing_bulb, + _mocked_relay, + _patch_config_flow_try_connect, + _patch_discovery, +) + +from tests.common import MockConfigEntry + + +async def test_discovery(hass: HomeAssistant): + """Test setting up discovery.""" + with _patch_discovery(), _patch_config_flow_try_connect(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + # test we can try again + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_setup, patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DEVICE: SERIAL}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == DEFAULT_ENTRY_TITLE + assert result3["data"] == {CONF_HOST: IP_ADDRESS} + mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() + + # ignore configured devices + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_discovery_but_cannot_connect(hass: HomeAssistant): + """Test we can discover the device but we cannot connect.""" + with _patch_discovery(), _patch_config_flow_try_connect(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DEVICE: SERIAL}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "cannot_connect" + + +async def test_discovery_with_existing_device_present(hass: HomeAssistant): + """Test setting up discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id="dd:dd:dd:dd:dd:dd" + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_config_flow_try_connect(no_device=True): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + # Now abort and make sure we can start over + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_DEVICE: SERIAL} + ) + assert result3["type"] == "create_entry" + assert result3["title"] == DEFAULT_ENTRY_TITLE + assert result3["data"] == { + CONF_HOST: IP_ADDRESS, + } + await hass.async_block_till_done() + + mock_setup_entry.assert_called_once() + + # ignore configured devices + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_discovery_no_device(hass: HomeAssistant): + """Test discovery without device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_manual(hass: HomeAssistant): + """Test manually setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + # Cannot connect (timeout) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + # Success + with _patch_discovery(), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup", return_value=True + ), patch(f"{MODULE}.async_setup_entry", return_value=True): + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + assert result4["type"] == "create_entry" + assert result4["title"] == DEFAULT_ENTRY_TITLE + assert result4["data"] == { + CONF_HOST: IP_ADDRESS, + } + + # Duplicate + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_manual_dns_error(hass: HomeAssistant): + """Test manually setup with unresolving host.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + class MockLifxConnectonDnsError: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + self.device = _mocked_failing_bulb() + + async def async_setup(self): + """Mock setup.""" + raise socket.gaierror() + + def async_stop(self): + """Mock teardown.""" + + # Cannot connect due to dns error + with _patch_discovery(no_device=True), patch( + "homeassistant.components.lifx.config_flow.LIFXConnection", + MockLifxConnectonDnsError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: "does.not.resolve"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_manual_no_capabilities(hass: HomeAssistant): + """Test manually setup without successful get_capabilities.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup", return_value=True + ), patch(f"{MODULE}.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["data"] == { + CONF_HOST: IP_ADDRESS, + } + + +async def test_discovered_by_discovery_and_dhcp(hass): + """Test we get the form with discovery and abort for dhcp source when we get both.""" + + with _patch_discovery(), _patch_config_flow_try_connect(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL + ), + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + with _patch_discovery(), _patch_config_flow_try_connect(): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress="00:00:00:00:00:00", hostname="mock_hostname" + ), + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_in_progress" + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.5", macaddress="00:00:00:00:00:01", hostname="mock_hostname" + ), + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "cannot_connect" + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname=LABEL, + name=LABEL, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + type="mock_type", + ), + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + {CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, + ), + ], +) +async def test_discovered_by_dhcp_or_discovery(hass, source, data): + """Test we can setup when discovered from dhcp or discovery.""" + + with _patch_discovery(), _patch_config_flow_try_connect(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_config_flow_try_connect(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_async_setup, patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_async_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["data"] == { + CONF_HOST: IP_ADDRESS, + } + assert mock_async_setup.called + assert mock_async_setup_entry.called + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname=LABEL, + name=LABEL, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + type="mock_type", + ), + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + {CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, + ), + ], +) +async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source, data): + """Test we abort if we cannot get the unique id when discovered from dhcp.""" + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_discovered_by_dhcp_updates_ip(hass): + """Update host from dhcp.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL + ), + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == IP_ADDRESS + + +async def test_refuse_relays(hass: HomeAssistant): + """Test we refuse to setup relays.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(device=_mocked_relay()), _patch_config_flow_try_connect( + device=_mocked_relay() + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/lifx/test_init.py b/tests/components/lifx/test_init.py new file mode 100644 index 00000000000..5424ae3c3fc --- /dev/null +++ b/tests/components/lifx/test_init.py @@ -0,0 +1,150 @@ +"""Tests for the lifx component.""" +from __future__ import annotations + +from datetime import timedelta +import socket +from unittest.mock import patch + +from homeassistant.components import lifx +from homeassistant.components.lifx import DOMAIN, discovery +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + IP_ADDRESS, + SERIAL, + MockFailingLifxCommand, + _mocked_bulb, + _mocked_failing_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_configuring_lifx_causes_discovery(hass): + """Test that specifying empty config does discovery.""" + start_calls = 0 + + class MockLifxDiscovery: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init discovery.""" + discovered = _mocked_bulb() + self.lights = {discovered.mac_addr: discovered} + + def start(self): + """Mock start.""" + nonlocal start_calls + start_calls += 1 + + def cleanup(self): + """Mock cleanup.""" + + with _patch_config_flow_try_connect(), patch.object( + discovery, "DEFAULT_TIMEOUT", 0 + ), patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert start_calls == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert start_calls == 1 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + assert start_calls == 2 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) + await hass.async_block_till_done() + assert start_calls == 3 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30)) + await hass.async_block_till_done() + assert start_calls == 4 + + +async def test_config_entry_reload(hass): + """Test that a config entry can be reloaded.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.LOADED + await hass.config_entries.async_unload(already_migrated_config_entry.entry_id) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_config_entry_retry(hass): + """Test that a config entry can be retried.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ), _patch_device(no_device=True): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_get_version_fails(hass): + """Test we handle get version failing.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.product = None + bulb.host_firmware_version = None + bulb.get_version = MockFailingLifxCommand(bulb) + + with _patch_discovery(device=bulb), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_dns_error_at_startup(hass): + """Test we handle get version failing.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_failing_bulb() + + class MockLifxConnectonDnsError: + """Mock lifx connection with a dns error.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + self.device = bulb + + async def async_setup(self): + """Mock setup.""" + raise socket.gaierror() + + def async_stop(self): + """Mock teardown.""" + + # Cannot connect due to dns error + with _patch_discovery(device=bulb), patch( + "homeassistant.components.lifx.LIFXConnection", + MockLifxConnectonDnsError, + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py new file mode 100644 index 00000000000..5b641e850f2 --- /dev/null +++ b/tests/components/lifx/test_light.py @@ -0,0 +1,993 @@ +"""Tests for the lifx integration light platform.""" + +from datetime import timedelta +from unittest.mock import patch + +import aiolifx_effects +import pytest + +from homeassistant.components import lifx +from homeassistant.components.lifx import DOMAIN +from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES +from homeassistant.components.lifx.manager import SERVICE_EFFECT_COLORLOOP +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + ATTR_TRANSITION, + ATTR_XY_COLOR, + DOMAIN as LIGHT_DOMAIN, + ColorMode, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + IP_ADDRESS, + MAC_ADDRESS, + SERIAL, + MockFailingLifxCommand, + MockLifxCommand, + MockMessage, + _mocked_brightness_bulb, + _mocked_bulb, + _mocked_bulb_new_firmware, + _mocked_light_strip, + _mocked_white_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_light_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "1.2.3.4"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, SERIAL)} + ) + assert device.identifiers == {(DOMAIN, SERIAL)} + + +async def test_light_unique_id_new_firmware(hass: HomeAssistant) -> None: + """Test a light unique id with newer firmware.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "1.2.3.4"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers=set(), + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + ) + assert device.identifiers == {(DOMAIN, SERIAL)} + + +@patch("homeassistant.components.lifx.light.COLOR_ZONE_POPULATE_DELAY", 0) +async def test_light_strip(hass: HomeAssistant) -> None: + """Test a light strip.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_light_strip() + bulb.power_level = 65535 + bulb.color = [65535, 65535, 65535, 65535] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == ColorMode.HS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (360.0, 100.0) + assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) + assert attributes[ATTR_XY_COLOR] == (0.701, 0.299) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + bulb.set_color_zones.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + bulb.set_color_zones.reset_mock() + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + # Single color uses the fast path + assert bulb.set_color.calls[0][0][0] == [1820, 19660, 65535, 3500] + bulb.set_color.reset_mock() + assert len(bulb.set_color_zones.calls) == 0 + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 10, 30)}, + blocking=True, + ) + # Single color uses the fast path + assert bulb.set_color.calls[0][0][0] == [64643, 62964, 65535, 3500] + bulb.set_color.reset_mock() + assert len(bulb.set_color_zones.calls) == 0 + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.3, 0.7)}, + blocking=True, + ) + # Single color uses the fast path + assert bulb.set_color.calls[0][0][0] == [15848, 65535, 65535, 3500] + bulb.set_color.reset_mock() + assert len(bulb.set_color_zones.calls) == 0 + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + # multiple zones in effect and we are changing the brightness + # we need to do each zone individually + assert len(bulb.set_color.calls) == 0 + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [0, 65535, 32896, 3500], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + call_dict = bulb.set_color_zones.calls[1][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [54612, 65535, 32896, 3500], + "duration": 0, + "end_index": 1, + "start_index": 1, + } + call_dict = bulb.set_color_zones.calls[7][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 1, + "color": [46420, 65535, 32896, 3500], + "duration": 0, + "end_index": 7, + "start_index": 7, + } + bulb.set_color_zones.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [0, 2], + }, + blocking=True, + ) + # set a two zones + assert len(bulb.set_color.calls) == 0 + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [0, 0, 65535, 3500], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + call_dict = bulb.set_color_zones.calls[1][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 1, + "color": [0, 0, 65535, 3500], + "duration": 0, + "end_index": 2, + "start_index": 2, + } + bulb.set_color_zones.reset_mock() + + bulb.get_color_zones.reset_mock() + bulb.set_power.reset_mock() + + bulb.power_level = 0 + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 255, 255), ATTR_ZONES: [3]}, + blocking=True, + ) + # set a one zone + assert len(bulb.set_power.calls) == 2 + assert len(bulb.get_color_zones.calls) == 2 + assert len(bulb.set_color.calls) == 0 + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 1, + "color": [0, 0, 65535, 3500], + "duration": 0, + "end_index": 3, + "start_index": 3, + } + bulb.get_color_zones.reset_mock() + bulb.set_power.reset_mock() + bulb.set_color_zones.reset_mock() + + bulb.set_color_zones = MockFailingLifxCommand(bulb) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + bulb.set_color_zones = MockLifxCommand(bulb) + bulb.get_color_zones = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + bulb.get_color_zones = MockLifxCommand(bulb) + bulb.get_color = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + +async def test_color_light_with_temp( + hass: HomeAssistant, mock_effect_conductor +) -> None: + """Test a color light with temp.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.power_level = 65535 + bulb.color = [65535, 65535, 65535, 65535] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == ColorMode.HS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (360.0, 100.0) + assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) + assert attributes[ATTR_XY_COLOR] == (0.701, 0.299) + + bulb.color = [32000, None, 32000, 6000] + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (31.007, 6.862) + assert attributes[ATTR_RGB_COLOR] == (255, 246, 237) + assert attributes[ATTR_XY_COLOR] == (0.339, 0.338) + bulb.color = [65535, 65535, 65535, 65535] + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [65535, 65535, 25700, 65535] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [1820, 19660, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 30, 80)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [63107, 57824, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.46, 0.376)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [4956, 30583, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_colorloop"}, + blocking=True, + ) + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectColorloop) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT_COLORLOOP, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectColorloop) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_pulse"}, + blocking=True, + ) + assert len(mock_effect_conductor.stop.mock_calls) == 1 + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectPulse) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_stop"}, + blocking=True, + ) + assert len(mock_effect_conductor.stop.mock_calls) == 2 + + +async def test_white_bulb(hass: HomeAssistant) -> None: + """Test a white bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_white_bulb() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ] + assert attributes[ATTR_COLOR_TEMP] == 166 + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 25700, 6000] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 32000, 2500] + bulb.set_color.reset_mock() + + +async def test_config_zoned_light_strip_fails(hass): + """Test we handle failure to update zones.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + light_strip = _mocked_light_strip() + entity_id = "light.my_bulb" + + class MockFailingLifxCommand: + """Mock a lifx command that fails on the 3rd try.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.call_count = 0 + + def __call__(self, callb=None, *args, **kwargs): + """Call command.""" + self.call_count += 1 + response = None if self.call_count >= 3 else MockMessage() + if callb: + callb(self.bulb, response) + + light_strip.get_color_zones = MockFailingLifxCommand(light_strip) + + with _patch_discovery(device=light_strip), _patch_device(device=light_strip): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + assert hass.states.get(entity_id).state == STATE_OFF + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_white_light_fails(hass): + """Test we handle failure to power on off.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_white_bulb() + entity_id = "light.my_bulb" + + bulb.set_power = MockFailingLifxCommand(bulb) + + with _patch_discovery(device=bulb), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + assert hass.states.get(entity_id).state == STATE_OFF + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + bulb.set_power = MockLifxCommand(bulb) + bulb.set_color = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [1, 0, 3, 6535] + bulb.set_color.reset_mock() + + +async def test_brightness_bulb(hass: HomeAssistant) -> None: + """Test a brightness only bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_brightness_bulb() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.BRIGHTNESS, + ] + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 25700, 6000] + bulb.set_color.reset_mock() + + +async def test_transitions_brightness_only(hass: HomeAssistant) -> None: + """Test transitions with a brightness only device.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_brightness_bulb() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.BRIGHTNESS, + ] + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + bulb.power_level = 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 5, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is True + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + + bulb.power_level = 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 5, ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is True + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + + await hass.async_block_till_done() + bulb.get_color.reset_mock() + + # Ensure we force an update after the transition + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(bulb.get_color.calls) == 2 + + +async def test_transitions_color_bulb(hass: HomeAssistant) -> None: + """Test transitions with a color bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + bulb.power_level = 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is False + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 0} # already off + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_RGB_COLOR: (255, 5, 10), + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [65316, 64249, 25700, 3500] + assert bulb.set_power.calls[0][0][0] is True + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + bulb.power_level = 12800 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_RGB_COLOR: (5, 5, 10), + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + ATTR_BRIGHTNESS: 200, + }, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [43690, 32767, 51400, 3500] + call_dict = bulb.set_color.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + await hass.async_block_till_done() + bulb.get_color.reset_mock() + + # Ensure we force an update after the transition + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(bulb.get_color.calls) == 2 + + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is False + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + +async def test_infrared_color_bulb(hass: HomeAssistant) -> None: + """Test setting infrared with a color bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_INFRARED: 100, + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert bulb.set_infrared.calls[0][0][0] == 25700 + + +async def test_color_bulb_is_actually_off(hass: HomeAssistant) -> None: + """Test setting a color when we think a bulb is on but its actually off.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + + class MockLifxCommandActuallyOff: + """Mock a lifx command that will update our power level state.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.calls = [] + + def __call__(self, *args, **kwargs): + """Call command.""" + bulb.power_level = 0 + if callb := kwargs.get("callb"): + callb(self.bulb, MockMessage()) + self.calls.append([args, kwargs]) + + bulb.set_color = MockLifxCommandActuallyOff(bulb) + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_RGB_COLOR: (100, 100, 100), + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [0, 0, 25700, 3500] + assert len(bulb.set_power.calls) == 1 diff --git a/tests/components/lifx/test_migration.py b/tests/components/lifx/test_migration.py new file mode 100644 index 00000000000..0f00034590b --- /dev/null +++ b/tests/components/lifx/test_migration.py @@ -0,0 +1,281 @@ +"""Tests the lifx migration.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from homeassistant import setup +from homeassistant.components import lifx +from homeassistant.components.lifx import DOMAIN, discovery +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + IP_ADDRESS, + LABEL, + MAC_ADDRESS, + SERIAL, + _mocked_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_migration_device_online_end_to_end( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test migration from single config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + light_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=dr.format_mac(SERIAL), + original_name=LABEL, + device_id=device.id, + ) + + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + migrated_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + migrated_entry = entry + break + + assert migrated_entry is not None + + assert device.config_entries == {migrated_entry.entry_id} + assert light_entity_reg.config_entry_id == migrated_entry.entry_id + assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + legacy_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + legacy_entry = entry + break + + assert legacy_entry is None + + +async def test_discovery_is_more_frequent_during_migration( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test that discovery is more frequent during migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=dr.format_mac(SERIAL), + original_name=LABEL, + device_id=device.id, + ) + + bulb = _mocked_bulb() + start_calls = 0 + + class MockLifxDiscovery: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init discovery.""" + self.bulb = bulb + self.lights = {} + + def start(self): + """Mock start.""" + nonlocal start_calls + start_calls += 1 + # Discover the bulb so we can complete migration + # and verify we switch back to normal discovery + # interval + if start_calls == 4: + self.lights = {self.bulb.mac_addr: self.bulb} + + def cleanup(self): + """Mock cleanup.""" + + with _patch_device(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), patch.object(discovery, "DEFAULT_TIMEOUT", 0), patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert start_calls == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert start_calls == 1 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + assert start_calls == 3 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + assert start_calls == 4 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) + await hass.async_block_till_done() + assert start_calls == 5 + + +async def test_migration_device_online_end_to_end_after_downgrade( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test migration from single config entry can happen again after a downgrade.""" + config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + config_entry.add_to_hass(hass) + + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + light_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=SERIAL, + original_name=LABEL, + device_id=device.id, + ) + + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + assert device.config_entries == {config_entry.entry_id} + assert light_entity_reg.config_entry_id == config_entry.entry_id + assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] + + legacy_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + legacy_entry = entry + break + + assert legacy_entry is None + + +async def test_migration_device_online_end_to_end_ignores_other_devices( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test migration from single config entry.""" + legacy_config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + legacy_config_entry.add_to_hass(hass) + + other_domain_config_entry = MockConfigEntry( + domain="other_domain", data={}, unique_id="other_domain" + ) + other_domain_config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=legacy_config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + other_device = device_reg.async_get_or_create( + config_entry_id=other_domain_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "556655665566")}, + name=LABEL, + ) + light_entity_reg = entity_reg.async_get_or_create( + config_entry=legacy_config_entry, + platform=DOMAIN, + domain="light", + unique_id=SERIAL, + original_name=LABEL, + device_id=device.id, + ) + ignored_entity_reg = entity_reg.async_get_or_create( + config_entry=other_domain_config_entry, + platform=DOMAIN, + domain="sensor", + unique_id="00:00:00:00:00:00_sensor", + original_name=LABEL, + device_id=device.id, + ) + garbage_entity_reg = entity_reg.async_get_or_create( + config_entry=legacy_config_entry, + platform=DOMAIN, + domain="sensor", + unique_id="garbage", + original_name=LABEL, + device_id=other_device.id, + ) + + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + new_entry = None + legacy_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + legacy_entry = entry + else: + new_entry = entry + + assert new_entry is not None + assert legacy_entry is None + + assert device.config_entries == {legacy_config_entry.entry_id} + assert light_entity_reg.config_entry_id == legacy_config_entry.entry_id + assert ignored_entity_reg.config_entry_id == other_domain_config_entry.entry_id + assert garbage_entity_reg.config_entry_id == legacy_config_entry.entry_id + + assert er.async_entries_for_config_entry(entity_reg, legacy_config_entry) == [] + assert dr.async_entries_for_config_entry(device_reg, legacy_config_entry) == [] diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index cfae178f792..18d5dff80db 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -85,7 +85,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -93,5 +93,5 @@ async def test_options(hass): user_input={CONF_DEFAULT_TRANSITION: 12}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEFAULT_TRANSITION: 12} diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index 4804ab83aca..1b84e3e8552 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -11,10 +11,10 @@ async def test_config_flow(hass, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get(f"sensor.{DOMAIN}") @@ -32,5 +32,5 @@ async def test_already_setup(hass, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index f65e9d8a6af..94b94c2ec93 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -41,10 +41,10 @@ async def webhook_id(hass, locative_client): result = await hass.config_entries.flow.async_init( "locative", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 6c4908a2ad5..ec4f2183a9a 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -27,7 +27,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.setup import async_setup_component @@ -51,7 +51,6 @@ def set_utc(hass): hass.config.set_time_zone("UTC") -@callback async def _async_mock_logbook_platform(hass: HomeAssistant) -> None: class MockLogbookPlatform: """Mock a logbook platform.""" diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 536d370f16a..2f1d69b8d47 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -66,7 +66,7 @@ async def test_step_import( flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -86,18 +86,18 @@ async def test_full_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test-other"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "http://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Logi Circle ({})".format("testId") @@ -115,7 +115,7 @@ async def test_abort_if_no_implementation_registered(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -128,21 +128,21 @@ async def test_abort_if_already_setup(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() result = await flow.async_step_auth() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "external_setup" @@ -161,7 +161,7 @@ async def test_abort_if_authorize_fails( mock_logi_circle.authorize.side_effect = side_effect result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "external_error" result = await flow.async_step_auth() @@ -173,7 +173,7 @@ async def test_not_pick_implementation_if_only_one(hass): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py index e24b9c92221..daa35f31845 100644 --- a/tests/components/lookin/test_config_flow.py +++ b/tests/components/lookin/test_config_flow.py @@ -10,11 +10,7 @@ from homeassistant import config_entries from homeassistant.components.lookin.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_TITLE, @@ -45,7 +41,7 @@ async def test_manual_setup(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: IP_ADDRESS} assert result["title"] == DEFAULT_ENTRY_TITLE assert len(mock_setup_entry.mock_calls) == 1 @@ -70,7 +66,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -123,7 +119,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_get_info(), patch( @@ -132,7 +128,7 @@ async def test_discovered_zeroconf(hass): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == {CONF_HOST: IP_ADDRESS} assert result2["title"] == DEFAULT_ENTRY_TITLE assert mock_async_setup_entry.called @@ -151,7 +147,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "127.0.0.2" @@ -167,7 +163,7 @@ async def test_discovered_zeroconf_cannot_connect(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -182,5 +178,5 @@ async def test_discovered_zeroconf_unknown_exception(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 595d4397200..25d41c0c2d0 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.luftdaten.const import CONF_SENSOR_ID from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -36,7 +32,7 @@ async def test_duplicate_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -48,7 +44,7 @@ async def test_communication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -58,7 +54,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"} assert "flow_id" in result2 @@ -69,7 +65,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -85,7 +81,7 @@ async def test_invalid_sensor( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -95,7 +91,7 @@ async def test_invalid_sensor( user_input={CONF_SENSOR_ID: 11111}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"} assert "flow_id" in result2 @@ -106,7 +102,7 @@ async def test_invalid_sensor( user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -124,7 +120,7 @@ async def test_step_user( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -136,7 +132,7 @@ async def test_step_user( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SENSOR_ID: 12345, diff --git a/tests/components/luftdaten/test_sensor.py b/tests/components/luftdaten/test_sensor.py index 3cf4426d500..f3d0f1c0b1f 100644 --- a/tests/components/luftdaten/test_sensor.py +++ b/tests/components/luftdaten/test_sensor.py @@ -29,71 +29,73 @@ async def test_luftdaten_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - entry = entity_registry.async_get("sensor.temperature") + entry = entity_registry.async_get("sensor.sensor_12345_temperature") assert entry assert entry.device_id assert entry.unique_id == "12345_temperature" - state = hass.states.get("sensor.temperature") + state = hass.states.get("sensor.sensor_12345_temperature") assert state assert state.state == "22.3" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Temperature" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Temperature" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.humidity") + entry = entity_registry.async_get("sensor.sensor_12345_humidity") assert entry assert entry.device_id assert entry.unique_id == "12345_humidity" - state = hass.states.get("sensor.humidity") + state = hass.states.get("sensor.sensor_12345_humidity") assert state assert state.state == "34.7" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Humidity" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Humidity" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pressure") + entry = entity_registry.async_get("sensor.sensor_12345_pressure") assert entry assert entry.device_id assert entry.unique_id == "12345_pressure" - state = hass.states.get("sensor.pressure") + state = hass.states.get("sensor.sensor_12345_pressure") assert state assert state.state == "98545.0" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pressure" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Pressure" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pressure_at_sealevel") + entry = entity_registry.async_get("sensor.sensor_12345_pressure_at_sealevel") assert entry assert entry.device_id assert entry.unique_id == "12345_pressure_at_sealevel" - state = hass.states.get("sensor.pressure_at_sealevel") + state = hass.states.get("sensor.sensor_12345_pressure_at_sealevel") assert state assert state.state == "103102.13" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pressure at sealevel" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Pressure at sealevel" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pm10") + entry = entity_registry.async_get("sensor.sensor_12345_pm10") assert entry assert entry.device_id assert entry.unique_id == "12345_P1" - state = hass.states.get("sensor.pm10") + state = hass.states.get("sensor.sensor_12345_pm10") assert state assert state.state == "8.5" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "PM10" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 PM10" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( @@ -102,15 +104,15 @@ async def test_luftdaten_sensors( ) assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pm2_5") + entry = entity_registry.async_get("sensor.sensor_12345_pm2_5") assert entry assert entry.device_id assert entry.unique_id == "12345_P2" - state = hass.states.get("sensor.pm2_5") + state = hass.states.get("sensor.sensor_12345_pm2_5") assert state assert state.state == "4.07" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "PM2.5" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 PM2.5" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 821bf07cf08..b5e8271d351 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -101,7 +101,7 @@ async def test_bridge_cannot_connect(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -124,7 +124,7 @@ async def test_bridge_cannot_connect_unknown_error(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -144,7 +144,7 @@ async def test_bridge_invalid_ssl_error(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -171,7 +171,7 @@ async def test_duplicate_bridge_import(hass): data=entry_mock_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index bdf1e359673..54cd842f0ee 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -11,12 +11,12 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.lutron_caseta import ( ATTR_ACTION, ATTR_AREA_NAME, - ATTR_BUTTON_NUMBER, ATTR_DEVICE_NAME, ATTR_SERIAL, ATTR_TYPE, ) from homeassistant.components.lutron_caseta.const import ( + ATTR_LEAP_BUTTON_NUMBER, DOMAIN, LUTRON_CASETA_BUTTON_EVENT, MANUFACTURER, @@ -51,7 +51,23 @@ MOCK_BUTTON_DEVICES = [ "type": "Pico3ButtonRaiseLower", "model": "PJ2-3BRL-GXX-X01", "serial": 43845548, - } + }, + { + "Name": "Front Steps Sunnata Keypad", + "ID": 3, + "Area": {"Name": "Front Steps"}, + "Buttons": [ + {"Number": 7}, + {"Number": 8}, + {"Number": 9}, + {"Number": 10}, + {"Number": 11}, + ], + "leap_name": "Front Steps_Front Steps Sunnata Keypad", + "type": "SunnataKeypad_3ButtonRaiseLower", + "model": "PJ2-3BRL-GXX-X01", + "serial": 43845547, + }, ] @@ -144,12 +160,11 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg): async def test_if_fires_on_button_event(hass, calls, device_reg): """Test for press trigger firing.""" - - config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) - data: LutronCasetaData = hass.data[DOMAIN][config_entry_id] - dr_button_devices = data.button_devices - device_id = list(dr_button_devices)[0] - device = dr_button_devices[device_id] + await _async_setup_lutron_with_picos(hass, device_reg) + device = MOCK_BUTTON_DEVICES[0] + dr = device_registry.async_get(hass) + dr_device = dr.async_get_device(identifiers={(DOMAIN, device["serial"])}) + device_id = dr_device.id assert await async_setup_component( hass, automation.DOMAIN, @@ -175,7 +190,51 @@ async def test_if_fires_on_button_event(hass, calls, device_reg): message = { ATTR_SERIAL: device.get("serial"), ATTR_TYPE: device.get("type"), - ATTR_BUTTON_NUMBER: 2, + ATTR_LEAP_BUTTON_NUMBER: 0, + ATTR_DEVICE_NAME: device["Name"], + ATTR_AREA_NAME: device.get("Area", {}).get("Name"), + ATTR_ACTION: "press", + } + hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_button_press" + + +async def test_if_fires_on_button_event_without_lip(hass, calls, device_reg): + """Test for press trigger firing on a device that does not support lip.""" + await _async_setup_lutron_with_picos(hass, device_reg) + device = MOCK_BUTTON_DEVICES[1] + dr = device_registry.async_get(hass) + dr_device = dr.async_get_device(identifiers={(DOMAIN, device["serial"])}) + device_id = dr_device.id + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "press", + CONF_SUBTYPE: "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + + message = { + ATTR_SERIAL: device.get("serial"), + ATTR_TYPE: device.get("type"), + ATTR_LEAP_BUTTON_NUMBER: 1, ATTR_DEVICE_NAME: device["Name"], ATTR_AREA_NAME: device.get("Area", {}).get("Name"), ATTR_ACTION: "press", @@ -214,7 +273,7 @@ async def test_validate_trigger_config_no_device(hass, calls, device_reg): message = { ATTR_SERIAL: "123", ATTR_TYPE: "any", - ATTR_BUTTON_NUMBER: 3, + ATTR_LEAP_BUTTON_NUMBER: 0, ATTR_DEVICE_NAME: "any", ATTR_AREA_NAME: "area", ATTR_ACTION: "press", @@ -259,7 +318,7 @@ async def test_validate_trigger_config_unknown_device(hass, calls, device_reg): message = { ATTR_SERIAL: "123", ATTR_TYPE: "any", - ATTR_BUTTON_NUMBER: 3, + ATTR_LEAP_BUTTON_NUMBER: 0, ATTR_DEVICE_NAME: "any", ATTR_AREA_NAME: "area", ATTR_ACTION: "press", diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index d9262e215fb..7faa63c2b1b 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -1,16 +1,17 @@ """Test the Honeywell Lyric config flow.""" -import asyncio from http import HTTPStatus from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.http import CONF_BASE_URL, DOMAIN as DOMAIN_HTTP -from homeassistant.components.lyric import config_flow +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.lyric.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -21,18 +22,12 @@ CLIENT_SECRET = "5678" @pytest.fixture() async def mock_impl(hass): """Mock implementation.""" - await setup.async_setup_component(hass, "http", {}) + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() - impl = config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, - CLIENT_ID, - CLIENT_SECRET, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "cred" ) - config_flow.OAuth2FlowHandler.async_register_implementation(hass, impl) - return impl async def test_abort_if_no_configuration(hass): @@ -40,26 +35,14 @@ async def test_abort_if_no_configuration(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" async def test_full_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, hass_client_no_auth, aioclient_mock, current_request_with_host, mock_impl ): """Check full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, - }, - DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -71,7 +54,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -98,7 +81,7 @@ async def test_full_flow( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == DOMAIN + assert result["data"]["auth_implementation"] == "cred" result["data"]["token"].pop("expires_at") assert result["data"]["token"] == { @@ -116,42 +99,10 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 -async def test_abort_if_authorization_timeout( - hass, mock_impl, current_request_with_host -): - """Check Somfy authorization timeout.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - flow = config_flow.OAuth2FlowHandler() - flow.hass = hass - - with patch.object( - mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError - ): - result = await flow.async_step_user() - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "authorize_url_timeout" - - async def test_reauthentication_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, hass_client_no_auth, aioclient_mock, current_request_with_host, mock_impl ): """Test reauthentication flow.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, - }, - DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"}, - }, - ) - old_entry = MockConfigEntry( domain=DOMAIN, unique_id=DOMAIN, @@ -196,7 +147,7 @@ async def test_reauthentication_flow( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index c6eeb9f99ad..2dc18b63d5b 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -37,10 +37,10 @@ async def webhook_id_with_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] @@ -57,10 +57,10 @@ async def webhook_id_without_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] diff --git a/tests/components/mazda/test_binary_sensor.py b/tests/components/mazda/test_binary_sensor.py index f2b272109c9..aa7cc306708 100644 --- a/tests/components/mazda/test_binary_sensor.py +++ b/tests/components/mazda/test_binary_sensor.py @@ -16,7 +16,7 @@ async def test_binary_sensors(hass): # Driver Door state = hass.states.get("binary_sensor.my_mazda3_driver_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "off" @@ -27,7 +27,7 @@ async def test_binary_sensors(hass): # Passenger Door state = hass.states.get("binary_sensor.my_mazda3_passenger_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "on" @@ -38,7 +38,7 @@ async def test_binary_sensors(hass): # Rear Left Door state = hass.states.get("binary_sensor.my_mazda3_rear_left_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "off" @@ -49,7 +49,7 @@ async def test_binary_sensors(hass): # Rear Right Door state = hass.states.get("binary_sensor.my_mazda3_rear_right_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "off" @@ -90,7 +90,7 @@ async def test_electric_vehicle_binary_sensors(hass): # Plugged In state = hass.states.get("binary_sensor.my_mazda3_plugged_in") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged In" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged in" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG assert state.state == "on" entry = entity_registry.async_get("binary_sensor.my_mazda3_plugged_in") diff --git a/tests/components/mazda/test_button.py b/tests/components/mazda/test_button.py index 71b16434ee3..81ad175020e 100644 --- a/tests/components/mazda/test_button.py +++ b/tests/components/mazda/test_button.py @@ -22,7 +22,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_start_engine" state = hass.states.get("button.my_mazda3_start_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine" entry = entity_registry.async_get("button.my_mazda3_stop_engine") @@ -30,7 +30,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_stop_engine" state = hass.states.get("button.my_mazda3_stop_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") @@ -38,7 +38,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights" assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") @@ -47,7 +47,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights" ) assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" @@ -69,7 +69,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_start_engine" state = hass.states.get("button.my_mazda3_start_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine" entry = entity_registry.async_get("button.my_mazda3_stop_engine") @@ -77,7 +77,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_stop_engine" state = hass.states.get("button.my_mazda3_stop_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") @@ -85,7 +85,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights" assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") @@ -94,7 +94,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights" ) assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" @@ -103,7 +103,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_refresh_vehicle_status" state = hass.states.get("button.my_mazda3_refresh_status") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh Status" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh status" assert state.attributes.get(ATTR_ICON) == "mdi:refresh" diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py index c2628f2aa11..1090d928952 100644 --- a/tests/components/mazda/test_config_flow.py +++ b/tests/components/mazda/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -71,7 +71,7 @@ async def test_account_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -95,7 +95,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -109,7 +109,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -120,7 +120,7 @@ async def test_form_account_locked(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -134,7 +134,7 @@ async def test_form_account_locked(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "account_locked"} @@ -206,7 +206,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -220,7 +220,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -249,7 +249,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -258,7 +258,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -288,7 +288,7 @@ async def test_reauth_account_locked(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -297,7 +297,7 @@ async def test_reauth_account_locked(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "account_locked"} @@ -327,7 +327,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -336,7 +336,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -366,7 +366,7 @@ async def test_reauth_unknown_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -375,7 +375,7 @@ async def test_reauth_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -405,7 +405,7 @@ async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Change the email and ensure the entry and its unique id gets @@ -419,5 +419,5 @@ async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: assert ( mock_config.unique_id == FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL[CONF_EMAIL] ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/mazda/test_device_tracker.py b/tests/components/mazda/test_device_tracker.py index 4af367c1c04..42a70fff1d4 100644 --- a/tests/components/mazda/test_device_tracker.py +++ b/tests/components/mazda/test_device_tracker.py @@ -20,7 +20,7 @@ async def test_device_tracker(hass): state = hass.states.get("device_tracker.my_mazda3_device_tracker") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device Tracker" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device tracker" assert state.attributes.get(ATTR_ICON) == "mdi:car" assert state.attributes.get(ATTR_LATITUDE) == 1.234567 assert state.attributes.get(ATTR_LONGITUDE) == -2.345678 diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index f2e9039397b..763e1490e89 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -32,7 +32,7 @@ async def test_sensors(hass): assert state assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "My Mazda3 Fuel Remaining Percentage" + == "My Mazda3 Fuel remaining percentage" ) assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -46,7 +46,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel Distance Remaining" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel distance remaining" ) assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS @@ -72,7 +72,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_front_left_tire_pressure") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front Left Tire Pressure" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front left tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -88,7 +88,7 @@ async def test_sensors(hass): assert state assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "My Mazda3 Front Right Tire Pressure" + == "My Mazda3 Front right tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -103,7 +103,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_rear_left_tire_pressure") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Tire Pressure" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -118,7 +118,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_rear_right_tire_pressure") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Tire Pressure" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -167,7 +167,7 @@ async def test_electric_vehicle_sensors(hass): # Charge Level state = hass.states.get("sensor.my_mazda3_charge_level") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charge Level" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charge level" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -179,7 +179,7 @@ async def test_electric_vehicle_sensors(hass): # Remaining Range state = hass.states.get("sensor.my_mazda3_remaining_range") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Remaining Range" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Remaining range" assert state.attributes.get(ATTR_ICON) == "mdi:ev-station" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT diff --git a/tests/components/meater/test_config_flow.py b/tests/components/meater/test_config_flow.py index 11312111311..0d05466e464 100644 --- a/tests/components/meater/test_config_flow.py +++ b/tests/components/meater/test_config_flow.py @@ -37,7 +37,7 @@ async def test_duplicate_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -83,7 +83,7 @@ async def test_user_flow(hass, mock_meater): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -93,7 +93,7 @@ async def test_user_flow(hass, mock_meater): result = await hass.config_entries.flow.async_configure(result["flow_id"], conf) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123", @@ -126,7 +126,7 @@ async def test_reauth_flow(hass, mock_meater): data=data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] is None @@ -136,7 +136,7 @@ async def test_reauth_flow(hass, mock_meater): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" config_entry = hass.config_entries.async_entries(DOMAIN)[0] diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index eceb7e9ec4f..f0666a11545 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,6 +1,5 @@ """Test the base functions of the media player.""" import asyncio -import base64 from http import HTTPStatus from unittest.mock import patch @@ -14,39 +13,6 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.setup import async_setup_component -async def test_get_image(hass, hass_ws_client, caplog): - """Test get image via WS command.""" - await async_setup_component( - hass, "media_player", {"media_player": {"platform": "demo"}} - ) - await hass.async_block_till_done() - - client = await hass_ws_client(hass) - - with patch( - "homeassistant.components.media_player.MediaPlayerEntity." - "async_get_media_image", - return_value=(b"image", "image/jpeg"), - ): - await client.send_json( - { - "id": 5, - "type": "media_player_thumbnail", - "entity_id": "media_player.bedroom", - } - ) - - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"]["content_type"] == "image/jpeg" - assert msg["result"]["content"] == base64.b64encode(b"image").decode("utf-8") - - assert "media_player_thumbnail is deprecated" in caplog.text - - async def test_get_image_http(hass, hass_client_no_auth): """Test get image via http command.""" await async_setup_component( diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 3025356d8fb..c9fde5c8ff7 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -16,10 +16,10 @@ async def test_tracking_home(hass, mock_weather): # Test the hourly sensor is disabled by default registry = er.async_get(hass) - state = hass.states.get("weather.test_home_hourly") + state = hass.states.get("weather.forecast_test_home_hourly") assert state is None - entry = registry.async_get("weather.test_home_hourly") + entry = registry.async_get("weather.forecast_test_home_hourly") assert entry assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION @@ -50,7 +50,7 @@ async def test_not_tracking_home(hass, mock_weather): WEATHER_DOMAIN, DOMAIN, "10-20-hourly", - suggested_object_id="somewhere_hourly", + suggested_object_id="forecast_somewhere_hourly", disabled_by=None, ) diff --git a/tests/components/met_eireann/test_config_flow.py b/tests/components/met_eireann/test_config_flow.py index 50060541be5..334e4a52ac3 100644 --- a/tests/components/met_eireann/test_config_flow.py +++ b/tests/components/met_eireann/test_config_flow.py @@ -64,7 +64,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == test_data.get("name") assert result["data"] == test_data @@ -85,11 +85,11 @@ async def test_flow_entry_already_exists(hass): result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Create the second entry and assert that it is aborted result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 29c08e41d1d..295e8a1e5e8 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -121,7 +121,7 @@ async def test_user(hass, client_single): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided with search returning only 1 place @@ -130,7 +130,7 @@ async def test_user(hass, client_single): context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_1_LAT}, {CITY_1_LON}" assert result["title"] == f"{CITY_1}" assert result["data"][CONF_LATITUDE] == str(CITY_1_LAT) @@ -146,14 +146,14 @@ async def test_user_list(hass, client_multiple): context={"source": SOURCE_USER}, data={CONF_CITY: CITY_2_NAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "cities" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_CITY: f"{CITY_3};{CITY_3_LAT};{CITY_3_LON}"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_3_LAT}, {CITY_3_LON}" assert result["title"] == f"{CITY_3}" assert result["data"][CONF_LATITUDE] == str(CITY_3_LAT) @@ -168,7 +168,7 @@ async def test_import(hass, client_multiple): context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_2_NAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_2_LAT}, {CITY_2_LON}" assert result["title"] == f"{CITY_2}" assert result["data"][CONF_LATITUDE] == str(CITY_2_LAT) @@ -183,7 +183,7 @@ async def test_search_failed(hass, client_empty): data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_CITY: "empty"} @@ -201,7 +201,7 @@ async def test_abort_if_already_setup(hass, client_single): context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same CITY same postal code (flow) @@ -210,7 +210,7 @@ async def test_abort_if_already_setup(hass, client_single): context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -226,7 +226,7 @@ async def test_options_flow(hass: HomeAssistant): assert config_entry.options == {} result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # Default @@ -234,7 +234,7 @@ async def test_options_flow(hass: HomeAssistant): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_MODE] == FORECAST_MODE_DAILY # Manual @@ -243,5 +243,5 @@ async def test_options_flow(hass: HomeAssistant): result["flow_id"], user_input={CONF_MODE: FORECAST_MODE_HOURLY}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_MODE] == FORECAST_MODE_HOURLY diff --git a/tests/components/meteoclimatic/test_config_flow.py b/tests/components/meteoclimatic/test_config_flow.py index e5daaea1978..4b94862553e 100644 --- a/tests/components/meteoclimatic/test_config_flow.py +++ b/tests/components/meteoclimatic/test_config_flow.py @@ -42,7 +42,7 @@ async def test_user(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -51,7 +51,7 @@ async def test_user(hass, client): context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == TEST_STATION_CODE assert result["title"] == TEST_STATION_NAME assert result["data"][CONF_STATION_CODE] == TEST_STATION_CODE @@ -68,7 +68,7 @@ async def test_not_found(hass): context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "not_found" @@ -84,5 +84,5 @@ async def test_unknown_error(hass): context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/mikrotik/__init__.py b/tests/components/mikrotik/__init__.py index 6f67eea1a0a..cebbd982350 100644 --- a/tests/components/mikrotik/__init__.py +++ b/tests/components/mikrotik/__init__.py @@ -1,4 +1,7 @@ """Tests for the Mikrotik component.""" +from unittest.mock import patch + +from homeassistant.components import mikrotik from homeassistant.components.mikrotik.const import ( CONF_ARP_PING, CONF_DETECTION_TIME, @@ -14,6 +17,8 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) +from tests.common import MockConfigEntry + MOCK_DATA = { CONF_NAME: "Mikrotik", CONF_HOST: "0.0.0.0", @@ -130,3 +135,38 @@ ARP_DATA = [ "disabled": False, }, ] + + +async def setup_mikrotik_entry(hass, **kwargs): + """Set up Mikrotik integration successfully.""" + support_wireless = kwargs.get("support_wireless", True) + dhcp_data = kwargs.get("dhcp_data", DHCP_DATA) + wireless_data = kwargs.get("wireless_data", WIRELESS_DATA) + + def mock_command(self, cmd, params=None): + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: + return support_wireless + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: + return dhcp_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: + return wireless_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.ARP]: + return ARP_DATA + return {} + + config_entry = MockConfigEntry( + domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + if "force_dhcp" in kwargs: + config_entry.options = {**config_entry.options, "force_dhcp": True} + + if "arp_ping" in kwargs: + config_entry.options = {**config_entry.options, "arp_ping": True} + + with patch("librouteros.connect"), patch.object( + mikrotik.hub.MikrotikData, "command", new=mock_command + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index b4c087a436d..704bf92066e 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -89,14 +89,14 @@ async def test_flow_works(hass, api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Home router" assert result["data"][CONF_NAME] == "Home router" assert result["data"][CONF_HOST] == "0.0.0.0" @@ -115,7 +115,7 @@ async def test_options(hass, api): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device_tracker" result = await hass.config_entries.options.async_configure( @@ -127,7 +127,7 @@ async def test_options(hass, api): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_DETECTION_TIME: 30, CONF_ARP_PING: True, @@ -179,7 +179,7 @@ async def test_connection_error(hass, conn_error): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -193,7 +193,7 @@ async def test_wrong_credentials(hass, auth_error): result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index fbbb016d09f..e3efe6bd39d 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -1,13 +1,14 @@ """The tests for the Mikrotik device tracker platform.""" from datetime import timedelta +from freezegun import freeze_time import pytest from homeassistant.components import mikrotik import homeassistant.components.device_tracker as device_tracker +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util.dt import utcnow from . import ( DEVICE_2_WIRELESS, @@ -17,12 +18,10 @@ from . import ( MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA, + setup_mikrotik_entry, ) -from .test_hub import setup_mikrotik_entry -from tests.common import MockConfigEntry, patch - -DEFAULT_DETECTION_TIME = timedelta(seconds=300) +from tests.common import MockConfigEntry, async_fire_time_changed, patch @pytest.fixture @@ -56,24 +55,11 @@ def mock_command(self, cmd, params=None): return {} -async def test_platform_manually_configured(hass): - """Test that nothing happens when configuring mikrotik through device tracker platform.""" - assert ( - await async_setup_component( - hass, - device_tracker.DOMAIN, - {device_tracker.DOMAIN: {"platform": "mikrotik"}}, - ) - is False - ) - assert mikrotik.DOMAIN not in hass.data - - async def test_device_trackers(hass, mock_device_registry_devices): """Test device_trackers created by mikrotik.""" # test devices are added from wireless list only - hub = await setup_mikrotik_entry(hass) + await setup_mikrotik_entry(hass) device_1 = hass.states.get("device_tracker.device_1") assert device_1 is not None @@ -90,7 +76,7 @@ async def test_device_trackers(hass, mock_device_registry_devices): # test device_2 is added after connecting to wireless network WIRELESS_DATA.append(DEVICE_2_WIRELESS) - await hub.async_refresh() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") @@ -104,26 +90,72 @@ async def test_device_trackers(hass, mock_device_registry_devices): # test state remains home if last_seen consider_home_interval del WIRELESS_DATA[1] # device 2 is removed from wireless list - hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( - minutes=4 - ) - await hub.async_update() - await hass.async_block_till_done() + with freeze_time(utcnow() + timedelta(minutes=4)): + async_fire_time_changed(hass, utcnow() + timedelta(minutes=4)) + await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") - assert device_2.state != "not_home" + assert device_2.state == "home" # test state changes to away if last_seen > consider_home_interval - hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( - minutes=5 - ) - await hub.async_refresh() - await hass.async_block_till_done() + with freeze_time(utcnow() + timedelta(minutes=6)): + async_fire_time_changed(hass, utcnow() + timedelta(minutes=6)) + await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") assert device_2.state == "not_home" +async def test_force_dhcp(hass, mock_device_registry_devices): + """Test updating hub that supports wireless with forced dhcp method.""" + + # hub supports wireless by default, force_dhcp is enabled to override + await setup_mikrotik_entry(hass, force_dhcp=False) + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 + assert device_1.state == "home" + # device_2 is not on the wireless list but it is still added from DHCP + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "home" + + +async def test_hub_not_support_wireless(hass, mock_device_registry_devices): + """Test device_trackers created when hub doesn't support wireless.""" + + await setup_mikrotik_entry(hass, support_wireless=False) + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 + assert device_1.state == "home" + # device_2 is added from DHCP + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "home" + + +async def test_arp_ping_success(hass, mock_device_registry_devices): + """Test arp ping devices to confirm they are connected.""" + + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=True): + await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + # test wired device_2 show as home if arp ping returns True + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "home" + + +async def test_arp_ping_timeout(hass, mock_device_registry_devices): + """Test arp ping timeout so devices are shown away.""" + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=False): + await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + # test wired device_2 show as not_home if arp ping times out + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "not_home" + + async def test_device_trackers_numerical_name(hass, mock_device_registry_devices): """Test device_trackers created by mikrotik with numerical device name.""" @@ -164,6 +196,13 @@ async def test_restoring_devices(hass): suggested_object_id="device_2", config_entry=config_entry, ) + registry.async_get_or_create( + device_tracker.DOMAIN, + mikrotik.DOMAIN, + "00:00:00:00:00:03", + suggested_object_id="device_3", + config_entry=config_entry, + ) await setup_mikrotik_entry(hass) @@ -174,3 +213,22 @@ async def test_restoring_devices(hass): device_2 = hass.states.get("device_tracker.device_2") assert device_2 is not None assert device_2.state == "not_home" + # device_3 is not on the DHCP list or wireless list + # so it won't be restored. + device_3 = hass.states.get("device_tracker.device_3") + assert device_3 is None + + +async def test_update_failed(hass, mock_device_registry_devices): + """Test failing to connect during update.""" + + await setup_mikrotik_entry(hass) + + with patch.object( + mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect + ): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == STATE_UNAVAILABLE diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py deleted file mode 100644 index 1e056071236..00000000000 --- a/tests/components/mikrotik/test_hub.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Test Mikrotik hub.""" -from unittest.mock import patch - -from homeassistant.components import mikrotik - -from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA - -from tests.common import MockConfigEntry - - -async def setup_mikrotik_entry(hass, **kwargs): - """Set up Mikrotik integration successfully.""" - support_wireless = kwargs.get("support_wireless", True) - dhcp_data = kwargs.get("dhcp_data", DHCP_DATA) - wireless_data = kwargs.get("wireless_data", WIRELESS_DATA) - - def mock_command(self, cmd, params=None): - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: - return support_wireless - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: - return dhcp_data - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: - return wireless_data - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.ARP]: - return ARP_DATA - return {} - - config_entry = MockConfigEntry( - domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS - ) - config_entry.add_to_hass(hass) - - if "force_dhcp" in kwargs: - config_entry.options = {**config_entry.options, "force_dhcp": True} - - if "arp_ping" in kwargs: - config_entry.options = {**config_entry.options, "arp_ping": True} - - with patch("librouteros.connect"), patch.object( - mikrotik.hub.MikrotikData, "command", new=mock_command - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - return hass.data[mikrotik.DOMAIN][config_entry.entry_id] - - -async def test_update_failed(hass): - """Test failing to connect during update.""" - - hub = await setup_mikrotik_entry(hass) - - with patch.object( - mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect - ): - await hub.async_refresh() - - assert not hub.last_update_success - - -async def test_hub_not_support_wireless(hass): - """Test updating hub devices when hub doesn't support wireless interfaces.""" - - # test that the devices are constructed from dhcp data - - hub = await setup_mikrotik_entry(hass, support_wireless=False) - - assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] - assert hub.api.devices["00:00:00:00:00:01"]._wireless_params is None - assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] - assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None - - -async def test_hub_support_wireless(hass): - """Test updating hub devices when hub support wireless interfaces.""" - - # test that the device list is from wireless data list - - hub = await setup_mikrotik_entry(hass) - - assert hub.api.support_wireless is True - assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] - assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] - - # devices not in wireless list will not be added - assert "00:00:00:00:00:02" not in hub.api.devices - - -async def test_force_dhcp(hass): - """Test updating hub devices with forced dhcp method.""" - - # test that the devices are constructed from dhcp data - - hub = await setup_mikrotik_entry(hass, force_dhcp=True) - - assert hub.api.support_wireless is True - assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] - assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] - - # devices not in wireless list are added from dhcp - assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] - assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None - - -async def test_arp_ping(hass): - """Test arp ping devices to confirm they are connected.""" - - # test device show as home if arp ping returns value - with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=True): - hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) - - assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None - assert hub.api.devices["00:00:00:00:00:02"].last_seen is not None - - # test device show as away if arp ping times out - with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=False): - hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) - - assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None - # this device is not wireless so it will show as away - assert hub.api.devices["00:00:00:00:00:02"].last_seen is None diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 5ac408928d8..3d7927174b5 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -39,7 +39,6 @@ async def test_successful_config_entry(hass): await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.LOADED - assert hass.data[DOMAIN][entry.entry_id] async def test_hub_conn_error(hass, mock_api): diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index ff2f7393c82..44fea69409b 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.mill.const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +15,7 @@ async def test_show_config_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -24,7 +24,7 @@ async def test_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -33,7 +33,7 @@ async def test_create_entry(hass): CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -72,7 +72,7 @@ async def test_flow_entry_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -81,7 +81,7 @@ async def test_flow_entry_already_exists(hass): CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -99,7 +99,7 @@ async def test_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -108,7 +108,7 @@ async def test_connection_error(hass): CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("mill.Mill.connect", return_value=False): result = await hass.config_entries.flow.async_configure( @@ -119,7 +119,7 @@ async def test_connection_error(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -128,7 +128,7 @@ async def test_local_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -137,7 +137,7 @@ async def test_local_create_entry(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -180,7 +180,7 @@ async def test_local_flow_entry_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -189,7 +189,7 @@ async def test_local_flow_entry_already_exists(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -219,7 +219,7 @@ async def test_local_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -228,7 +228,7 @@ async def test_local_connection_error(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -243,5 +243,5 @@ async def test_local_connection_error(hass): test_data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py index ac617eb938c..0eb334763d6 100644 --- a/tests/components/min_max/test_config_flow.py +++ b/tests/components/min_max/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.min_max.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -32,7 +32,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My min_max" assert result["data"] == {} assert result["options"] == { @@ -92,7 +92,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "entity_ids") == input_sensors1 @@ -107,7 +107,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "type": "mean", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_ids": input_sensors2, "name": "My min_max", diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 9717fa0052b..7d6b8227396 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.minecraft_server.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -86,7 +82,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -97,7 +93,7 @@ async def test_invalid_ip(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4 ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_ip"} @@ -122,7 +118,7 @@ async def test_same_host(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -136,7 +132,7 @@ async def test_port_too_small(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_SMALL ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_port"} @@ -150,7 +146,7 @@ async def test_port_too_large(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_LARGE ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_port"} @@ -164,7 +160,7 @@ async def test_connection_failed(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -178,7 +174,7 @@ async def test_connection_succeeded_with_srv_record(hass: HomeAssistant) -> None DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_SRV ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_SRV[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_SRV[CONF_NAME] assert result["data"][CONF_HOST] == USER_INPUT_SRV[CONF_HOST] @@ -194,7 +190,7 @@ async def test_connection_succeeded_with_host(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME] assert result["data"][CONF_HOST] == "mc.dummyserver.com" @@ -213,7 +209,7 @@ async def test_connection_succeeded_with_ip4(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4 ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_IPV4[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_IPV4[CONF_NAME] assert result["data"][CONF_HOST] == "1.1.1.1" @@ -232,7 +228,7 @@ async def test_connection_succeeded_with_ip6(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV6 ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_IPV6[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_IPV6[CONF_NAME] assert result["data"][CONF_HOST] == "::ffff:0101:0101" diff --git a/tests/components/mjpeg/test_config_flow.py b/tests/components/mjpeg/test_config_flow.py index cdf56ab97c0..3d66af21f0a 100644 --- a/tests/components/mjpeg/test_config_flow.py +++ b/tests/components/mjpeg/test_config_flow.py @@ -20,11 +20,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -39,7 +35,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -55,7 +51,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Spy cam" assert result2.get("data") == {} assert result2.get("options") == { @@ -85,7 +81,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -102,7 +98,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"username": "invalid_auth"} assert "flow_id" in result2 @@ -121,7 +117,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Sky cam" assert result3.get("data") == {} assert result3.get("options") == { @@ -147,7 +143,7 @@ async def test_connection_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -164,7 +160,7 @@ async def test_connection_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"mjpeg_url": "cannot_connect"} assert "flow_id" in result2 @@ -188,7 +184,7 @@ async def test_connection_error( }, ) - assert result3.get("type") == RESULT_TYPE_FORM + assert result3.get("type") == FlowResultType.FORM assert result3.get("step_id") == SOURCE_USER assert result3.get("errors") == {"still_image_url": "cannot_connect"} assert "flow_id" in result3 @@ -209,7 +205,7 @@ async def test_connection_error( }, ) - assert result4.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result4.get("type") == FlowResultType.CREATE_ENTRY assert result4.get("title") == "My cam" assert result4.get("data") == {} assert result4.get("options") == { @@ -247,7 +243,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -259,7 +255,7 @@ async def test_options_flow( """Test options config flow.""" result = await hass.config_entries.options.async_init(init_integration.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -288,7 +284,7 @@ async def test_options_flow( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "init" assert result2.get("errors") == {"mjpeg_url": "already_configured"} assert "flow_id" in result2 @@ -307,7 +303,7 @@ async def test_options_flow( }, ) - assert result3.get("type") == RESULT_TYPE_FORM + assert result3.get("type") == FlowResultType.FORM assert result3.get("step_id") == "init" assert result3.get("errors") == {"mjpeg_url": "cannot_connect"} assert "flow_id" in result3 @@ -326,7 +322,7 @@ async def test_options_flow( }, ) - assert result4.get("type") == RESULT_TYPE_FORM + assert result4.get("type") == FlowResultType.FORM assert result4.get("step_id") == "init" assert result4.get("errors") == {"still_image_url": "cannot_connect"} assert "flow_id" in result4 @@ -346,7 +342,7 @@ async def test_options_flow( }, ) - assert result5.get("type") == RESULT_TYPE_FORM + assert result5.get("type") == FlowResultType.FORM assert result5.get("step_id") == "init" assert result5.get("errors") == {"username": "invalid_auth"} assert "flow_id" in result5 @@ -363,7 +359,7 @@ async def test_options_flow( }, ) - assert result6.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result6.get("type") == FlowResultType.CREATE_ENTRY assert result6.get("data") == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_MJPEG_URL: "https://example.com/mjpeg", diff --git a/tests/components/moat/__init__.py b/tests/components/moat/__init__.py new file mode 100644 index 00000000000..358d338993f --- /dev/null +++ b/tests/components/moat/__init__.py @@ -0,0 +1,32 @@ +"""Tests for the Moat BLE integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_MOAT_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +MOAT_S2_SERVICE_INFO = BluetoothServiceInfo( + name="Moat_S2", + manufacturer_data={}, + service_data={ + "00005000-0000-1000-8000-00805f9b34fb": b"\xdfy\xe3\xa6\x12\xb3\xf5\x0b", + "00001000-0000-1000-8000-00805f9b34fb": ( + b"\xdfy\xe3\xa6\x12\xb3\x11S\xdbb\xfcbpq" b"\xf5\x0b\xff\xff" + ), + }, + service_uuids=[ + "00001000-0000-1000-8000-00805f9b34fb", + "00002000-0000-1000-8000-00805f9b34fb", + ], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + source="local", +) diff --git a/tests/components/moat/conftest.py b/tests/components/moat/conftest.py new file mode 100644 index 00000000000..1f7f00c8d2f --- /dev/null +++ b/tests/components/moat/conftest.py @@ -0,0 +1,8 @@ +"""Moat session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/moat/test_config_flow.py b/tests/components/moat/test_config_flow.py new file mode 100644 index 00000000000..7ceeb2ad73f --- /dev/null +++ b/tests/components/moat/test_config_flow.py @@ -0,0 +1,164 @@ +"""Test the Moat config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.moat.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import MOAT_S2_SERVICE_INFO, NOT_MOAT_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Moat S2 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_moat(hass): + """Test discovery via bluetooth not moat.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_MOAT_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Moat S2 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Moat S2 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/moat/test_sensor.py b/tests/components/moat/test_sensor.py new file mode 100644 index 00000000000..6424144106b --- /dev/null +++ b/tests/components/moat/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Moat sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.moat.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import MOAT_S2_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(MOAT_S2_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + temp_sensor = hass.states.get("sensor.moat_s2_eeff_voltage") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "3.061" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Moat S2 EEFF Voltage" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "V" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 0bc237b1c11..b7b95dff392 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -840,7 +840,7 @@ async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_clien @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("tag_scanned", store_event) diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index 19a98106c63..8dc7eccf25a 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -37,14 +37,14 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: com_port().device} @@ -56,7 +56,7 @@ async def test_flow_usb_cannot_connect(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -78,7 +78,7 @@ async def test_flow_user(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} result = await hass.config_entries.flow.async_init( @@ -86,7 +86,7 @@ async def test_flow_user(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -107,7 +107,7 @@ async def test_flow_user_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -116,7 +116,7 @@ async def test_flow_user_error(hass: HomeAssistant): result["flow_id"], user_input={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} @@ -129,7 +129,7 @@ async def test_flow_user_no_port_list(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -141,7 +141,7 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_confirm" result2 = await hass.config_entries.flow.async_init( @@ -150,5 +150,5 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant): data={}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 931d2918fe2..05cebb2fef5 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.modern_forms.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import init_integration @@ -37,7 +33,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result with patch( @@ -50,7 +46,7 @@ async def test_full_user_flow_implementation( assert result2.get("title") == "ModernFormsFan" assert "data" in result2 - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2["data"][CONF_HOST] == "192.168.1.123" assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF" assert len(mock_setup_entry.mock_calls) == 1 @@ -85,7 +81,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {CONF_NAME: "example"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result flow = flows[0] @@ -98,7 +94,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result2.get("title") == "example" - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -121,7 +117,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -150,7 +146,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -182,7 +178,7 @@ async def test_zeroconf_confirm_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -218,7 +214,7 @@ async def test_user_device_exists_abort( }, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -252,5 +248,5 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/moehlenhoff_alpha2/test_config_flow.py b/tests/components/moehlenhoff_alpha2/test_config_flow.py index ccfa98718e5..a1f98454015 100644 --- a/tests/components/moehlenhoff_alpha2/test_config_flow.py +++ b/tests/components/moehlenhoff_alpha2/test_config_flow.py @@ -5,11 +5,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.moehlenhoff_alpha2.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -33,7 +29,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch("moehlenhoff_alpha2.Alpha2Base.update_data", mock_update_data), patch( @@ -46,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_BASE_NAME assert result2["data"] == {"host": MOCK_BASE_HOST} assert len(mock_setup_entry.mock_calls) == 1 @@ -70,7 +66,7 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: data={"host": MOCK_BASE_HOST}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -87,7 +83,7 @@ async def test_form_cannot_connect_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -102,5 +98,5 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 98275b052e4..c9f5bb9dee7 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -107,7 +107,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -115,5 +115,5 @@ async def test_options_flow(hass): user_input={CONF_SOURCE_1: "one", CONF_SOURCE_4: "", CONF_SOURCE_5: "five"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == {"1": "one", "5": "five"} diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py index 4bfb61166aa..9dfc186d492 100644 --- a/tests/components/moon/test_config_flow.py +++ b/tests/components/moon/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.moon.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +21,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -34,7 +30,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Moon" assert result2.get("data") == {} @@ -52,7 +48,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -67,6 +63,6 @@ async def test_import_flow( data={CONF_NAME: "My Moon"}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "My Moon" assert result.get("data") == {} diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index bb9e5dcc157..6f13c97a3be 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -16,9 +16,9 @@ from homeassistant.components.moon.sensor import ( STATE_WAXING_CRESCENT, STATE_WAXING_GIBBOUS, ) -from homeassistant.const import ATTR_ICON +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -52,12 +52,20 @@ async def test_moon_day( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.moon") + state = hass.states.get("sensor.moon_phase") assert state assert state.state == native_value assert state.attributes[ATTR_ICON] == icon + assert state.attributes[ATTR_FRIENDLY_NAME] == "Moon Phase" entity_registry = er.async_get(hass) - entry = entity_registry.async_get("sensor.moon") + entry = entity_registry.async_get("sensor.moon_phase") assert entry assert entry.unique_id == mock_config_entry.entry_id + + device_registry = dr.async_get(hass) + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Moon" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 57ab45d9dbb..23defadde9a 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -372,7 +372,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -380,7 +380,7 @@ async def test_options_flow(hass): user_input={const.CONF_WAIT_FOR_PUSH: False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_WAIT_FOR_PUSH: False, } diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 8ba9fb07715..3e2db3bf897 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -265,12 +265,12 @@ async def test_get_stream_from_camera(aiohttp_server: Any, hass: HomeAssistant) client = create_mock_motioneye_client() client.get_camera_stream_url = Mock( - return_value=f"http://localhost:{stream_server.port}/" + return_value=f"http://127.0.0.1:{stream_server.port}/" ) config_entry = create_mock_motioneye_config_entry( hass, data={ - CONF_URL: f"http://localhost:{stream_server.port}", + CONF_URL: f"http://127.0.0.1:{stream_server.port}", # The port won't be used as the client is a mock. CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, }, @@ -351,13 +351,13 @@ async def test_camera_option_stream_url_template( config_entry = create_mock_motioneye_config_entry( hass, data={ - CONF_URL: f"http://localhost:{stream_server.port}", + CONF_URL: f"http://127.0.0.1:{stream_server.port}", # The port won't be used as the client is a mock. CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, }, options={ CONF_STREAM_URL_TEMPLATE: ( - f"http://localhost:{stream_server.port}/" "{{ name }}/{{ id }}" + f"http://127.0.0.1:{stream_server.port}/" "{{ name }}/{{ id }}" ) }, ) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 269a2b8a4c4..6fe38ccf7a1 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -79,13 +79,13 @@ async def test_hassio_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "motionEye"} assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result2.get("type") == data_entry_flow.FlowResultType.FORM assert result2.get("step_id") == "user" assert "flow_id" in result2 @@ -109,7 +109,7 @@ async def test_hassio_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3.get("title") == "Add-on" assert result3.get("data") == { CONF_URL: TEST_URL, @@ -287,7 +287,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert dict(config_entry.data) == {**new_data, CONF_WEBHOOK_ID: "test-webhook-id"} @@ -337,7 +337,7 @@ async def test_duplicate(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_client.async_client_close.called @@ -354,7 +354,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -369,7 +369,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -378,14 +378,14 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result2.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result2.get("type") == data_entry_flow.FlowResultType.ABORT assert result2.get("reason") == "already_in_progress" @@ -397,12 +397,12 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result2.get("type") == data_entry_flow.FlowResultType.FORM assert "flow_id" in result2 mock_client = create_mock_motioneye_client() @@ -426,7 +426,7 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 flows = hass.config_entries.flow.async_progress() @@ -449,7 +449,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -460,7 +460,7 @@ async def test_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -492,7 +492,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -511,7 +511,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert result["data"][CONF_STREAM_URL_TEMPLATE] == "http://moo" diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index f4a72829046..7d127902f3d 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -60,6 +60,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -971,3 +972,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = alarm_control_panel.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 20037a88d1c..658af79f20f 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -44,6 +44,7 @@ from .test_common import ( help_test_setting_attribute_with_template, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1079,3 +1080,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = binary_sensor.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 8748ef3be4d..68db846c91c 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -37,6 +37,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -482,3 +483,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = button.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 84bf4181a2c..c6d116b6a74 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -36,6 +36,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -346,3 +347,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = camera.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index c633f267e76..aec83a85227 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -55,6 +55,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1881,3 +1882,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = climate.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 92feaa3c109..fe1f4003f0b 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -2,7 +2,7 @@ import copy from datetime import datetime import json -from unittest.mock import ANY, patch +from unittest.mock import ANY, MagicMock, patch import yaml @@ -11,6 +11,7 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.const import MQTT_DISCONNECTED from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -1670,6 +1671,25 @@ async def help_test_reload_with_config(hass, caplog, tmp_path, config): assert "" in caplog.text +async def help_test_entry_reload_with_new_config(hass, tmp_path, new_config): + """Test reloading with supplied config.""" + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump(new_config) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file), patch( + "paho.mqtt.client.Client" + ) as mock_client: + mock_client().connect = lambda *args: 0 + # reload the config entry + assert await hass.config_entries.async_reload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.LOADED + await hass.async_block_till_done() + + async def help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ): @@ -1782,6 +1802,7 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): domain: [new_config_1, new_config_2, new_config_3], } await help_test_reload_with_config(hass, caplog, tmp_path, new_config) + await hass.async_block_till_done() assert len(hass.states.async_all(domain)) == 3 @@ -1792,6 +1813,12 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): async def help_test_setup_manual_entity_from_yaml(hass, platform, config): """Help to test setup from yaml through configuration entry.""" + calls = MagicMock() + + async def mock_reload(hass): + """Mock reload.""" + calls() + config_structure = {mqtt.DOMAIN: {platform: config}} await async_setup_component(hass, mqtt.DOMAIN, config_structure) @@ -1799,7 +1826,71 @@ async def help_test_setup_manual_entity_from_yaml(hass, platform, config): entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_reload_manual_mqtt_items", + side_effect=mock_reload, + ), patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = lambda *args: 0 assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + calls.assert_called_once() + + +async def help_test_unload_config_entry(hass, tmp_path, newconfig): + """Test unloading the MQTT config entry.""" + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump(newconfig) + new_yaml_config_file.write_text(new_yaml_config) + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + await hass.async_block_till_done() + + +async def help_test_unload_config_entry_with_platform( + hass, + mqtt_mock_entry_with_yaml_config, + tmp_path, + domain, + config, +): + """Test unloading the MQTT config entry with a specific platform domain.""" + # prepare setup through configuration.yaml + config_setup = copy.deepcopy(config) + config_setup["name"] = "config_setup" + config_name = config_setup + assert await async_setup_component(hass, domain, {domain: [config_setup]}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + # prepare setup through discovery + discovery_setup = copy.deepcopy(config) + discovery_setup["name"] = "discovery_setup" + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup) + ) + await hass.async_block_till_done() + + # check if both entities were setup correctly + config_setup_entity = hass.states.get(f"{domain}.config_setup") + assert config_setup_entity + + discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup") + assert discovery_setup_entity + + await help_test_unload_config_entry(hass, tmp_path, config_setup) + + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup) + ) + await hass.async_block_till_done() + + # check if both entities were unloaded correctly + config_setup_entity = hass.states.get(f"{domain}.{config_name}") + assert config_setup_entity is None + + discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup") + assert discovery_setup_entity is None diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 6fe781335f0..e40397fd1d4 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -145,12 +145,17 @@ async def test_manual_config_starts_discovery_flow( async def test_manual_config_set( - hass, mock_try_connection, mock_finish_setup, mqtt_client_mock + hass, + mock_try_connection, + mock_finish_setup, + mqtt_client_mock, ): """Test manual config does not create an entry, and entry can be setup late.""" # MQTT config present in yaml config assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}}) await hass.async_block_till_done() + # do not try to reload + del hass.data["mqtt_reload_needed"] assert len(mock_finish_setup.mock_calls) == 0 mock_try_connection.return_value = True @@ -218,7 +223,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -277,7 +282,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -289,7 +294,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -311,8 +316,8 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] is None + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345, @@ -352,7 +357,7 @@ async def test_disable_birth_will( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -364,7 +369,7 @@ async def test_disable_birth_will( mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -386,8 +391,8 @@ async def test_disable_birth_will( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] is None + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345, @@ -448,7 +453,7 @@ async def test_option_flow_default_suggested_values( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -472,7 +477,7 @@ async def test_option_flow_default_suggested_values( mqtt.CONF_PASSWORD: "p4ss", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: True, @@ -506,11 +511,11 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Test updated default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "another-broker", @@ -529,7 +534,7 @@ async def test_option_flow_default_suggested_values( result["flow_id"], user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: False, @@ -563,7 +568,7 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Make sure all MQTT related jobs are done before ending the test await hass.async_block_till_done() @@ -717,7 +722,7 @@ async def test_try_connection_with_advanced_parameters( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -741,7 +746,7 @@ async def test_try_connection_with_advanced_parameters( mqtt.CONF_PASSWORD: "p4ss", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" # check if the username and password was set from config flow and not from configuration.yaml diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index c0d63cec1b4..b1c162073ed 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -73,6 +73,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -986,7 +987,7 @@ async def test_set_tilt_templated_and_attributes( "set_position_topic": "set-position-topic", "set_position_template": "{{position-1}}", "tilt_command_template": "{" - '"enitity_id": "{{ entity_id }}",' + '"entity_id": "{{ entity_id }}",' '"value": {{ value }},' '"tilt_position": {{ tilt_position }}' "}", @@ -1008,7 +1009,7 @@ async def test_set_tilt_templated_and_attributes( mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 45,"tilt_position": 45}', + '{"entity_id": "cover.test","value": 45,"tilt_position": 45}', 0, False, ) @@ -1022,7 +1023,7 @@ async def test_set_tilt_templated_and_attributes( ) mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 100,"tilt_position": 100}', + '{"entity_id": "cover.test","value": 100,"tilt_position": 100}', 0, False, ) @@ -1036,7 +1037,7 @@ async def test_set_tilt_templated_and_attributes( ) mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 0,"tilt_position": 0}', + '{"entity_id": "cover.test","value": 0,"tilt_position": 0}', 0, False, ) @@ -1050,7 +1051,7 @@ async def test_set_tilt_templated_and_attributes( ) mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 100,"tilt_position": 100}', + '{"entity_id": "cover.test","value": 100,"tilt_position": 100}', 0, False, ) @@ -3364,3 +3365,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = cover.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index a9eb9b20825..6708703ddbb 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,13 +1,20 @@ """The tests for the MQTT device tracker platform using configuration.yaml.""" +import json from unittest.mock import patch import pytest from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH +from homeassistant.config_entries import ConfigEntryDisabler from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, Platform from homeassistant.setup import async_setup_component -from .test_common import help_test_setup_manual_entity_from_yaml +from .test_common import ( + MockConfigEntry, + help_test_entry_reload_with_new_config, + help_test_setup_manual_entity_from_yaml, + help_test_unload_config_entry, +) from tests.common import async_fire_mqtt_message @@ -265,3 +272,114 @@ async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): await help_test_setup_manual_entity_from_yaml(hass, DOMAIN, config) assert hass.states.get(entity_id) is not None + + +async def test_unload_entry( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path +): + """Test unloading the config entry.""" + # setup through configuration.yaml + await mqtt_mock_entry_no_yaml_config() + dev_id = "jan" + entity_id = f"{DOMAIN}.{dev_id}" + topic = "/location/jan" + location = "home" + + hass.config.components = {"mqtt", "zone"} + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} + ) + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == location + + # setup through discovery + dev_id = "piet" + subscription = "/location/#" + domain = DOMAIN + discovery_config = { + "devices": {dev_id: subscription}, + "state_topic": "some-state", + "name": "piet", + } + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_config) + ) + await hass.async_block_till_done() + + # check that both entities were created + config_setup_entity = hass.states.get(f"{domain}.jan") + assert config_setup_entity + + discovery_setup_entity = hass.states.get(f"{domain}.piet") + assert discovery_setup_entity + + await help_test_unload_config_entry(hass, tmp_path, {}) + await hass.async_block_till_done() + + # check that both entities were unsubscribed and that the location was not processed + async_fire_mqtt_message(hass, "some-state", "not_home") + async_fire_mqtt_message(hass, "location/jan", "not_home") + await hass.async_block_till_done() + + config_setup_entity = hass.states.get(f"{domain}.jan") + assert config_setup_entity.state == location + + # the discovered tracker is an entity which state is removed at unload + discovery_setup_entity = hass.states.get(f"{domain}.piet") + assert discovery_setup_entity is None + + +async def test_reload_entry_legacy( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path +): + """Test reloading the config entry with manual MQTT items.""" + # setup through configuration.yaml + await mqtt_mock_entry_no_yaml_config() + entity_id = f"{DOMAIN}.jan" + topic = "location/jan" + location = "home" + + config = { + DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + } + hass.config.components = {"mqtt", "zone"} + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == location + + await help_test_entry_reload_with_new_config(hass, tmp_path, config) + await hass.async_block_till_done() + + location = "not_home" + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == location + + +async def test_setup_with_disabled_entry( + hass, mock_device_tracker_conf, caplog +) -> None: + """Test setting up the platform with a disabled config entry.""" + # Try to setup the platform with a disabled config entry + config_entry = MockConfigEntry( + domain="mqtt", data={}, disabled_by=ConfigEntryDisabler.USER + ) + config_entry.add_to_hass(hass) + topic = "location/jan" + + config = { + DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + } + hass.config.components = {"mqtt", "zone"} + + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + assert ( + "MQTT device trackers will be not available until the config entry is enabled" + in caplog.text + ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 842e1dc4106..37a59ef6b53 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -12,6 +12,8 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component +from .test_common import help_test_unload_config_entry + from tests.common import ( assert_lists_same, async_fire_mqtt_message, @@ -1372,3 +1374,53 @@ async def test_trigger_debug_info(hass, mqtt_mock_entry_no_yaml_config): == "homeassistant/device_automation/bla2/config" ) assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config2 + + +async def test_unload_entry(hass, calls, device_reg, mqtt_mock, tmp_path) -> None: + """Test unloading the MQTT entry.""" + + data1 = ( + '{ "automation_type":"trigger",' + ' "device":{"identifiers":["0AFFD2"]},' + ' "topic": "foobar/triggers/button1",' + ' "type": "button_short_press",' + ' "subtype": "button_1" }' + ) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "bla1", + "type": "button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press")}, + }, + }, + ] + }, + ) + + # Fake short press 1 + async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") + await hass.async_block_till_done() + assert len(calls) == 1 + + await help_test_unload_config_entry(hass, tmp_path, {}) + + # Fake short press 2 + async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") + await hass.async_block_till_done() + assert len(calls) == 1 diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index 8cc5d0b1070..7c486f9af74 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -158,7 +158,7 @@ async def test_entry_diagnostics( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index d185d3334d0..e4c6f44883a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -46,7 +46,7 @@ def entity_reg(hass): @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_subscribing_config_topic(hass, mqtt_mock_entry_no_yaml_config): @@ -1238,19 +1238,27 @@ async def test_no_implicit_state_topic_switch( @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register", } ], ) async def test_complex_discovery_topic_prefix( - hass, mqtt_mock_entry_no_yaml_config, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Tests handling of discovery topic prefix with multiple slashes.""" - await mqtt_mock_entry_no_yaml_config() + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register", + } + }, + ) + async_fire_mqtt_message( hass, ("my_home/homeassistant/register/binary_sensor/node1/object1/config"), diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index b9ca5e3888d..37dcefc9d3f 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -58,6 +58,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1910,3 +1911,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = fan.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 0301e9e0481..38dc634578f 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -60,6 +60,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1296,3 +1297,12 @@ async def test_config_schema_validation(hass): CONFIG_SCHEMA({DOMAIN: {platform: [config]}}) with pytest.raises(MultipleInvalid): CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}}) + + +async def test_unload_config_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = humidifier.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b435798c241..fe8f483adf2 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -4,7 +4,6 @@ import copy from datetime import datetime, timedelta from functools import partial import json -import logging import ssl from unittest.mock import ANY, AsyncMock, MagicMock, call, mock_open, patch @@ -17,6 +16,7 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, EVENT_HOMEASSISTANT_STARTED, @@ -32,7 +32,10 @@ from homeassistant.helpers.entity import Entity from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .test_common import help_test_setup_manual_entity_from_yaml +from .test_common import ( + help_test_entry_reload_with_new_config, + help_test_setup_manual_entity_from_yaml, +) from tests.common import ( MockConfigEntry, @@ -43,8 +46,6 @@ from tests.common import ( ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES -_LOGGER = logging.getLogger(__name__) - class RecordCallsPartial(partial): """Wrapper class for partial.""" @@ -106,6 +107,18 @@ def record_calls(calls): return record_calls +@pytest.fixture +def empty_mqtt_config(hass, tmp_path): + """Fixture to provide an empty config from yaml.""" + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config_file.write_text("") + + with patch.object( + hass_config, "YAML_CONFIG_FILE", new_yaml_config_file + ) as empty_config: + yield empty_config + + async def test_mqtt_connects_on_home_assistant_mqtt_setup( hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): @@ -115,14 +128,57 @@ async def test_mqtt_connects_on_home_assistant_mqtt_setup( async def test_mqtt_disconnects_on_home_assistant_stop( - hass, mqtt_mock_entry_no_yaml_config + hass, mqtt_mock_entry_no_yaml_config, mqtt_client_mock ): """Test if client stops on HA stop.""" - mqtt_mock = await mqtt_mock_entry_no_yaml_config() + await mqtt_mock_entry_no_yaml_config() hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() - assert mqtt_mock.async_disconnect.called + assert mqtt_client_mock.loop_stop.call_count == 1 + + +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_mqtt_await_ack_at_disconnect( + hass, +): + """Test if ACK is awaited correctly when disconnecting.""" + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 100 + rc = 0 + + with patch("paho.mqtt.client.Client") as mock_client: + mock_client().connect = MagicMock(return_value=0) + mock_client().publish = MagicMock(return_value=FakeInfo()) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, + ) + entry.add_to_hass(hass) + assert await mqtt.async_setup_entry(hass, entry) + mqtt_client = mock_client.return_value + + # publish from MQTT client without awaiting + hass.async_create_task( + mqtt.async_publish(hass, "test-topic", "some-payload", 0, False) + ) + await asyncio.sleep(0) + # Simulate late ACK callback from client with mid 100 + mqtt_client.on_publish(0, 0, 100) + # disconnect the MQTT client + await hass.async_stop() + await hass.async_block_till_done() + # assert the payload was sent through the client + assert mqtt_client.publish.called + assert mqtt_client.publish.call_args[0] == ( + "test-topic", + "some-payload", + 0, + False, + ) async def test_publish(hass, mqtt_mock_entry_no_yaml_config): @@ -225,7 +281,7 @@ async def test_command_template_value(hass): async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config): - """Test the rendering of enitity_variables.""" + """Test the rendering of entity variables.""" topic = "test/select" fake_state = ha.State("select.test", "milk") @@ -521,8 +577,11 @@ async def test_service_call_with_ascii_qos_retain_flags( assert not mqtt_mock.async_publish.call_args[0][3] -async def test_publish_function_with_bad_encoding_conditions(hass, caplog): - """Test internal publish function with bas use cases.""" +async def test_publish_function_with_bad_encoding_conditions( + hass, caplog, mqtt_mock_entry_no_yaml_config +): + """Test internal publish function with basic use cases.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_publish( hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None ) @@ -1190,7 +1249,7 @@ async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_ @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_restore_subscriptions_on_reconnect( @@ -1213,7 +1272,7 @@ async def test_restore_subscriptions_on_reconnect( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_restore_all_active_subscriptions_on_reconnect( @@ -1249,13 +1308,18 @@ async def test_restore_all_active_subscriptions_on_reconnect( assert mqtt_client_mock.subscribe.mock_calls == expected -async def test_initial_setup_logs_error(hass, caplog, mqtt_client_mock): +async def test_initial_setup_logs_error( + hass, caplog, mqtt_client_mock, empty_mqtt_config +): """Test for setup failure if initial client connection fails.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - + entry.add_to_hass(hass) mqtt_client_mock.connect.return_value = 1 - assert await mqtt.async_setup_entry(hass, entry) - await hass.async_block_till_done() + try: + assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() + except HomeAssistantError: + assert True assert "Failed to connect to MQTT server:" in caplog.text @@ -1298,6 +1362,7 @@ async def test_handle_mqtt_on_callback( async def test_publish_error(hass, caplog): """Test publish error.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) # simulate an Out of memory error with patch("paho.mqtt.client.Client") as mock_client: @@ -1365,6 +1430,7 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker", "password": "somepassword"}, ) + entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: mock_client().username_pw_set = mock_usename_password_set @@ -1420,18 +1486,20 @@ async def test_setup_manual_mqtt_empty_platform(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", []) -async def test_setup_mqtt_client_protocol(hass): +async def test_setup_mqtt_client_protocol(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT client protocol setup.""" - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={ - mqtt.CONF_BROKER: "test-broker", - mqtt.config_integration.CONF_PROTOCOL: "3.1", - }, - ) with patch("paho.mqtt.client.Client") as mock_client: + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + mqtt.config_integration.CONF_PROTOCOL: "3.1", + } + }, + ) mock_client.on_connect(return_value=0) - assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() # check if protocol setup was correctly assert mock_client.call_args[1]["protocol"] == 3 @@ -1467,15 +1535,18 @@ async def test_handle_mqtt_timeout_on_callback(hass, caplog): entry = MockConfigEntry( domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} ) - # Set up the integration - assert await mqtt.async_setup_entry(hass, entry) + entry.add_to_hass(hass) + # Make sure we are connected correctly mock_client.on_connect(mock_client, None, None, 0) + # Set up the integration + assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() # Now call we publish without simulating and ACK callback await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") await hass.async_block_till_done() - # The is no ACK so we should see a timeout in the log after publishing + # There is no ACK so we should see a timeout in the log after publishing assert len(mock_client.publish.mock_calls) == 1 assert "No ACK from MQTT server" in caplog.text @@ -1483,16 +1554,26 @@ async def test_handle_mqtt_timeout_on_callback(hass, caplog): async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplog): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = MagicMock(side_effect=OSError("Connection error")) assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() assert "Failed to connect to MQTT server due to exception:" in caplog.text -@pytest.mark.parametrize("insecure", [None, False, True]) +@pytest.mark.parametrize( + "config, insecure_param", + [ + ({"certificate": "auto"}, "not set"), + ({"certificate": "auto", "tls_insecure": False}, False), + ({"certificate": "auto", "tls_insecure": True}, True), + ], +) +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( - hass, insecure + hass, config, insecure_param, mqtt_mock_entry_with_yaml_config ): """Test setup uses bundled certs when certificate is set to auto and insecure.""" calls = [] @@ -1504,37 +1585,29 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( def mock_tls_insecure_set(insecure_param): insecure_check["insecure"] = insecure_param - config_item_data = {mqtt.CONF_BROKER: "test-broker", "certificate": "auto"} - if insecure is not None: - config_item_data["tls_insecure"] = insecure with patch("paho.mqtt.client.Client") as mock_client: mock_client().tls_set = mock_tls_set mock_client().tls_insecure_set = mock_tls_insecure_set - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data=config_item_data, + assert await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: config}, ) - - assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() assert calls import certifi expectedCertificate = certifi.where() - # assert mock_mqtt.mock_calls[0][1][2]["certificate"] == expectedCertificate assert calls[0][0] == expectedCertificate # test if insecure is set - assert ( - insecure_check["insecure"] == insecure - if insecure is not None - else insecure_check["insecure"] == "not set" - ) + assert insecure_check["insecure"] == insecure_param -async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): - """Test setup defaults to TLSv1 under python3.6.""" +async def test_tls_version(hass, mqtt_mock_entry_with_yaml_config): + """Test setup defaults for tls.""" calls = [] def mock_tls_set(certificate, certfile=None, keyfile=None, tls_version=None): @@ -1542,27 +1615,19 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): with patch("paho.mqtt.client.Client") as mock_client: mock_client().tls_set = mock_tls_set - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, + assert await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {"certificate": "auto"}}, ) - - assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() assert calls - - import sys - - if sys.hexversion >= 0x03060000: - expectedTlsVersion = ssl.PROTOCOL_TLS # pylint: disable=no-member - else: - expectedTlsVersion = ssl.PROTOCOL_TLSv1 - - assert calls[0][3] == expectedTlsVersion + assert calls[0][3] == ssl.PROTOCOL_TLS @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1595,7 +1660,7 @@ async def test_custom_birth_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1630,7 +1695,7 @@ async def test_default_birth_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], ) async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): @@ -1644,7 +1709,7 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_ @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1658,7 +1723,7 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_ ], ) async def test_delayed_birth_message( - hass, mqtt_client_mock, mqtt_config, mqtt_mock_entry_no_yaml_config + hass, mqtt_client_mock, mqtt_config_entry_data, mqtt_mock_entry_no_yaml_config ): """Test sending birth message does not happen until Home Assistant starts.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() @@ -1668,7 +1733,7 @@ async def test_delayed_birth_message( await hass.async_block_till_done() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config) + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1705,7 +1770,7 @@ async def test_delayed_birth_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1741,7 +1806,7 @@ async def test_default_will_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], ) async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): @@ -1752,7 +1817,7 @@ async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_c @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -2644,3 +2709,232 @@ async def test_config_schema_validation(hass): config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}} with pytest.raises(vol.MultipleInvalid): CONFIG_SCHEMA(config) + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_unload_config_entry( + hass, mqtt_mock, mqtt_client_mock, tmp_path, caplog +) -> None: + """Test unloading the MQTT entry.""" + assert hass.services.has_service(mqtt.DOMAIN, "dump") + assert hass.services.has_service(mqtt.DOMAIN, "publish") + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + # Publish just before unloading to test await cleanup + mqtt_client_mock.reset_mock() + mqtt.publish(hass, "just_in_time", "published", qos=0, retain=False) + + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({}) + new_yaml_config_file.write_text(new_yaml_config) + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + mqtt_client_mock.publish.assert_any_call("just_in_time", "published", 0, False) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + await hass.async_block_till_done() + assert not hass.services.has_service(mqtt.DOMAIN, "dump") + assert not hass.services.has_service(mqtt.DOMAIN, "publish") + assert "No ACK from MQTT server" not in caplog.text + + +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_setup_with_disabled_entry(hass, caplog) -> None: + """Test setting up the platform with a disabled config entry.""" + # Try to setup the platform with a disabled config entry + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={}, disabled_by=ConfigEntryDisabler.USER + ) + config_entry.add_to_hass(hass) + + config = {mqtt.DOMAIN: {}} + await async_setup_component(hass, mqtt.DOMAIN, config) + await hass.async_block_till_done() + + assert "MQTT will be not available until the config entry is enabled" in caplog.text + + +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_publish_or_subscribe_without_valid_config_entry(hass, caplog): + """Test internal publish function with bas use cases.""" + with pytest.raises(HomeAssistantError): + await mqtt.async_publish( + hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None + ) + with pytest.raises(HomeAssistantError): + await mqtt.async_subscribe(hass, "some-topic", lambda: None, qos=0) + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_reload_entry_with_new_config(hass, tmp_path): + """Test reloading the config entry with a new yaml config.""" + config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + config_yaml_new = { + "mqtt": { + "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] + }, + "light": [ + { + "platform": "mqtt", + "name": "test_new_legacy", + "command_topic": "test-topic_new", + } + ], + } + await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + assert hass.states.get("light.test_old1") is not None + + await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new) + assert hass.states.get("light.test_old1") is None + assert hass.states.get("light.test_new_modern") is not None + assert hass.states.get("light.test_new_legacy") is not None + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): + """Test disabling and enabling the config entry.""" + config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + config_yaml_new = { + "mqtt": { + "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] + }, + "light": [ + { + "platform": "mqtt", + "name": "test_new_legacy", + "command_topic": "test-topic_new", + } + ], + } + await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + assert hass.states.get("light.test_old1") is not None + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + + assert mqtt_config_entry.state is ConfigEntryState.LOADED + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump(config_yaml_new) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file), patch( + "paho.mqtt.client.Client" + ) as mock_client: + mock_client().connect = lambda *args: 0 + + # Late discovery of a light + config = '{"name": "abc", "command_topic": "test-topic"}' + async_fire_mqtt_message(hass, "homeassistant/light/abc/config", config) + + # Disable MQTT config entry + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, ConfigEntryDisabler.USER + ) + + await hass.async_block_till_done() + await hass.async_block_till_done() + # Assert that the discovery was still received + # but kipped the setup + assert ( + "MQTT integration is disabled, skipping setup of manually configured MQTT light" + in caplog.text + ) + + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + assert hass.states.get("light.test_old1") is None + + # Enable the entry again + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, None + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + assert hass.states.get("light.test_old1") is None + assert hass.states.get("light.test_new_modern") is not None + assert hass.states.get("light.test_new_legacy") is not None + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +@pytest.mark.parametrize( + "config, unique", + [ + ( + [ + { + "name": "test1", + "unique_id": "very_not_unique_deadbeef", + "command_topic": "test-topic_unique", + }, + { + "name": "test2", + "unique_id": "very_not_unique_deadbeef", + "command_topic": "test-topic_unique", + }, + ], + False, + ), + ( + [ + { + "name": "test1", + "unique_id": "very_unique_deadbeef1", + "command_topic": "test-topic_unique", + }, + { + "name": "test2", + "unique_id": "very_unique_deadbeef2", + "command_topic": "test-topic_unique", + }, + ], + True, + ), + ], +) +async def test_setup_manual_items_with_unique_ids( + hass, tmp_path, caplog, config, unique +): + """Test setup manual items is generating unique id's.""" + await help_test_setup_manual_entity_from_yaml(hass, "light", config) + + assert hass.states.get("light.test1") is not None + assert (hass.states.get("light.test2") is not None) == unique + assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique + + # reload and assert again + caplog.clear() + await help_test_entry_reload_with_new_config( + hass, tmp_path, {"mqtt": {"light": config}} + ) + + assert hass.states.get("light.test1") is not None + assert (hass.states.get("light.test2") is not None) == unique + assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique + + +async def test_remove_unknown_conf_entry_options(hass, mqtt_client_mock, caplog): + """Test unknown keys in config entry data is removed.""" + mqtt_config_entry_data = { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: {}, + mqtt.client.CONF_PROTOCOL: mqtt.const.PROTOCOL_311, + } + + entry = MockConfigEntry( + data=mqtt_config_entry_data, + domain=mqtt.DOMAIN, + title="MQTT", + ) + + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mqtt.client.CONF_PROTOCOL not in entry.data + assert ( + "The following unsupported configuration options were removed from the " + "MQTT config entry: {'protocol'}. Add them to configuration.yaml if they " + "are needed" + ) in caplog.text diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 4d8d8f24a3c..bfafc99a9e2 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -240,6 +240,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -3803,3 +3804,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = light.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 6e271d08651..2c96468057f 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -72,6 +72,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1266,3 +1267,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = light.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 1bf4183e60f..f6dc4a0ed6d 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -48,6 +48,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -748,3 +749,12 @@ async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = LOCK_DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 1db7c5e3463..458f1f740e1 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -57,6 +57,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -853,3 +854,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = number.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 3036565dad5..713410059fe 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -22,6 +22,7 @@ from .test_common import ( help_test_reloadable_late, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, ) DEFAULT_CONFIG = { @@ -237,3 +238,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = scene.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index c22bd43b86f..4c3a0523951 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -48,6 +48,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -687,3 +688,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = select.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index f30bcf43392..ab094c20b6f 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -58,6 +58,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1213,3 +1214,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = sensor.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 6da9682c1c7..13648f1c486 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -45,6 +45,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -975,3 +976,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = siren.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index ba23efc859c..af6c0f99f50 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -42,6 +42,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -664,3 +665,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = switch.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index f06dd6f5244..507c6d99bed 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -11,6 +11,8 @@ from homeassistant.const import Platform from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component +from .test_common import help_test_unload_config_entry + from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -797,3 +799,28 @@ async def test_cleanup_device_with_entity2( # Verify device registry entry is cleared device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None + + +async def test_unload_entry(hass, device_reg, mqtt_mock, tag_mock, tmp_path) -> None: + """Test unloading the MQTT entry.""" + + config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) + + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + + # Fake tag scan, should be processed + async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) + await hass.async_block_till_done() + tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) + + tag_mock.reset_mock() + + await help_test_unload_config_entry(hass, tmp_path, {}) + await hass.async_block_till_done() + + # Fake tag scan, should not be processed + async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) + await hass.async_block_till_done() + tag_mock.assert_not_called() diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index 967ad0dbcc4..451e9ec4b90 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -5,7 +5,7 @@ from mullvad_api import MullvadAPIError from homeassistant import config_entries, setup from homeassistant.components.mullvad.const import DOMAIN -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -16,7 +16,7 @@ async def test_form_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch( @@ -45,7 +45,7 @@ async def test_form_user_only_once(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -66,7 +66,7 @@ async def test_connection_error(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -87,5 +87,5 @@ async def test_unknown_error(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 67274cf1c78..aef0fa41fe5 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -32,7 +32,7 @@ async def test_form_create_entry_without_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -51,7 +51,7 @@ async def test_form_create_entry_without_auth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +62,7 @@ async def test_form_create_entry_with_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -80,7 +80,7 @@ async def test_form_create_entry_with_auth(hass): VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "credentials" result = await hass.config_entries.flow.async_configure( @@ -89,7 +89,7 @@ async def test_form_create_entry_with_auth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -120,7 +120,7 @@ async def test_reauth_successful(hass): data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -128,7 +128,7 @@ async def test_reauth_successful(hass): user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -152,7 +152,7 @@ async def test_reauth_unsuccessful(hass): data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -160,7 +160,7 @@ async def test_reauth_unsuccessful(hass): user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" @@ -189,7 +189,7 @@ async def test_form_with_auth_errors(hass, error): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "credentials" with patch( @@ -244,7 +244,7 @@ async def test_form_abort(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "device_unsupported" @@ -271,7 +271,7 @@ async def test_form_already_configured(hass): {"host": "1.1.1.1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -298,7 +298,7 @@ async def test_zeroconf(hass): if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" assert context["confirm_only"] is True @@ -313,7 +313,7 @@ async def test_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"] == {"host": "10.10.2.3"} assert len(mock_setup_entry.mock_calls) == 1 @@ -339,7 +339,7 @@ async def test_zeroconf_with_auth(hass): if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" @@ -359,7 +359,7 @@ async def test_zeroconf_with_auth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -380,7 +380,7 @@ async def test_zeroconf_host_already_configured(hass): context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -404,5 +404,5 @@ async def test_zeroconf_errors(hass, error): context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 7e187f1e2fd..14571c75588 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -85,7 +85,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -111,7 +111,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -148,7 +148,7 @@ async def test_reauth( new_entry = hass.config_entries.async_get_entry("my_entry") - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert new_entry.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index f86112ada75..fbdc2665879 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator import copy from dataclasses import dataclass, field import time -from typing import Any, Generator, TypeVar +from typing import Any, TypeVar from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.device import Device diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index 5c4194f46f6..86f05c613ed 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -17,6 +17,7 @@ from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING, StreamType from homeassistant.components.nest.const import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -210,11 +211,11 @@ async def test_camera_device( camera = hass.states.get("camera.my_camera") assert camera is not None assert camera.state == STATE_STREAMING + assert camera.attributes.get(ATTR_FRIENDLY_NAME) == "My Camera" registry = er.async_get(hass) entry = registry.async_get("camera.my_camera") assert entry.unique_id == f"{DEVICE_ID}-camera" - assert entry.original_name == "My Camera" assert entry.domain == "camera" device_registry = dr.async_get(hass) diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py index f199d2ec7dd..3784304ac8b 100644 --- a/tests/components/nest/test_config_flow_legacy.py +++ b/tests/components/nest/test_config_flow_legacy.py @@ -24,7 +24,7 @@ async def test_abort_if_single_instance_allowed(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -40,14 +40,14 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"flow_impl": "nest"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert ( result["description_placeholders"] @@ -69,7 +69,7 @@ async def test_full_flow_implementation(hass): ) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["tokens"] == {"access_token": "yoo"} assert result["data"]["impl_domain"] == "nest" assert result["title"] == "Nest (via configuration.yaml)" @@ -83,7 +83,7 @@ async def test_not_pick_implementation_if_only_one(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -99,7 +99,7 @@ async def test_abort_if_timeout_generating_auth_url(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -115,7 +115,7 @@ async def test_abort_if_exception_generating_auth_url(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -127,7 +127,7 @@ async def test_verify_code_timeout(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -137,7 +137,7 @@ async def test_verify_code_timeout(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "timeout"} @@ -150,7 +150,7 @@ async def test_verify_code_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -160,7 +160,7 @@ async def test_verify_code_invalid(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "invalid_pin"} @@ -173,7 +173,7 @@ async def test_verify_code_unknown_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -183,7 +183,7 @@ async def test_verify_code_unknown_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "unknown"} @@ -196,7 +196,7 @@ async def test_verify_code_exception(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -206,7 +206,7 @@ async def test_verify_code_exception(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "internal_error"} @@ -220,7 +220,7 @@ async def test_step_import(hass): flow = hass.config_entries.flow.async_progress()[0] result = await hass.config_entries.flow.async_configure(flow["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 83845586764..1fffcc9042b 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -160,7 +160,6 @@ async def test_event( entry = registry.async_get("camera.front") assert entry is not None assert entry.unique_id == "some-device-id-camera" - assert entry.original_name == "Front" assert entry.domain == "camera" device_registry = dr.async_get(hass) diff --git a/tests/components/nest/test_sensor_sdm.py b/tests/components/nest/test_sensor_sdm.py index bb49f13b3eb..0f98d0c05b4 100644 --- a/tests/components/nest/test_sensor_sdm.py +++ b/tests/components/nest/test_sensor_sdm.py @@ -13,6 +13,7 @@ import pytest from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -59,6 +60,7 @@ async def test_thermostat_device( assert temperature.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert temperature.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE assert temperature.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert temperature.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Temperature" humidity = hass.states.get("sensor.my_sensor_humidity") assert humidity is not None @@ -66,16 +68,15 @@ async def test_thermostat_device( assert humidity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert humidity.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY assert humidity.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert humidity.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Humidity" registry = er.async_get(hass) entry = registry.async_get("sensor.my_sensor_temperature") assert entry.unique_id == f"{DEVICE_ID}-temperature" - assert entry.original_name == "My Sensor Temperature" assert entry.domain == "sensor" entry = registry.async_get("sensor.my_sensor_humidity") assert entry.unique_id == f"{DEVICE_ID}-humidity" - assert entry.original_name == "My Sensor Humidity" assert entry.domain == "sensor" device_registry = dr.async_get(hass) @@ -195,11 +196,11 @@ async def test_device_with_unknown_type( temperature = hass.states.get("sensor.my_sensor_temperature") assert temperature is not None assert temperature.state == "25.1" + assert temperature.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Temperature" registry = er.async_get(hass) entry = registry.async_get("sensor.my_sensor_temperature") assert entry.unique_id == f"{DEVICE_ID}-temperature" - assert entry.original_name == "My Sensor Temperature" assert entry.domain == "sensor" device_registry = dr.async_get(hass) diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 2eaf713e8ee..0e10ce92288 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -32,8 +32,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): webhook_id = config_entry.data[CONF_WEBHOOK_ID] await hass.async_block_till_done() - camera_entity_indoor = "camera.netatmo_hall" - camera_entity_outdoor = "camera.netatmo_garden" + camera_entity_indoor = "camera.hall" + camera_entity_outdoor = "camera.garden" assert hass.states.get(camera_entity_indoor).state == "streaming" response = { "event_type": "off", @@ -95,7 +95,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( - "camera", "turn_off", service_data={"entity_id": "camera.netatmo_hall"} + "camera", "turn_off", service_data={"entity_id": "camera.hall"} ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( @@ -106,7 +106,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( - "camera", "turn_on", service_data={"entity_id": "camera.netatmo_hall"} + "camera", "turn_on", service_data={"entity_id": "camera.hall"} ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( @@ -130,7 +130,7 @@ async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_aut uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" stream_uri = uri + "/live/files/high/index.m3u8" - camera_entity_indoor = "camera.netatmo_hall" + camera_entity_indoor = "camera.hall" cam = hass.states.get(camera_entity_indoor) assert cam is not None @@ -161,7 +161,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) "6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,," ) stream_uri = uri + "/live/files/high/index.m3u8" - camera_entity_indoor = "camera.netatmo_garden" + camera_entity_indoor = "camera.garden" cam = hass.states.get(camera_entity_indoor) assert cam is not None @@ -188,7 +188,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "person": "Richard Doe", } @@ -205,7 +205,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth): ) data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", } with patch( @@ -231,7 +231,7 @@ async def test_service_set_person_away_invalid_person(hass, config_entry, netatm await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "person": "Batman", } @@ -259,7 +259,7 @@ async def test_service_set_persons_home_invalid_person( await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "persons": "Batman", } @@ -285,7 +285,7 @@ async def test_service_set_persons_home(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "persons": "John Doe", } @@ -312,7 +312,7 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_garden", + "entity_id": "camera.garden", "camera_light_mode": "on", } @@ -485,7 +485,7 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - camera_entity_indoor = "camera.netatmo_hall" + camera_entity_indoor = "camera.hall" with pytest.raises(Exception) as excinfo: await camera.async_get_image(hass, camera_entity_indoor) diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 30fb5fd3d47..ae13268eda3 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -34,7 +34,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await hass.config_entries.flow.async_init( @@ -50,7 +50,7 @@ async def test_abort_if_existing_entry(hass): type="mock_type", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -142,28 +142,28 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -200,28 +200,28 @@ async def test_option_flow_wrong_coordinates(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -289,7 +289,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -326,7 +326,7 @@ async def test_reauth( new_entry2 = hass.config_entries.async_entries(DOMAIN)[0] - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert new_entry2.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index a0992e7ea2c..433841f3878 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -27,7 +27,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth): await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await hass.async_block_till_done() - light_entity = "light.netatmo_garden" + light_entity = "light.garden" assert hass.states.get(light_entity).state == "unavailable" # Trigger light mode change diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index b1b5b11265a..9adc7423bd6 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -17,7 +17,7 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth): await hass.async_block_till_done() - prefix = "sensor.netatmo_mystation_" + prefix = "sensor.mystation_" assert hass.states.get(f"{prefix}temperature").state == "24.6" assert hass.states.get(f"{prefix}humidity").state == "36" @@ -34,13 +34,13 @@ async def test_public_weather_sensor(hass, config_entry, netatmo_auth): assert len(hass.states.async_all()) > 0 - prefix = "sensor.netatmo_home_max_" + prefix = "sensor.home_max_" assert hass.states.get(f"{prefix}temperature").state == "27.4" assert hass.states.get(f"{prefix}humidity").state == "76" assert hass.states.get(f"{prefix}pressure").state == "1014.4" - prefix = "sensor.netatmo_home_avg_" + prefix = "sensor.home_avg_" assert hass.states.get(f"{prefix}temperature").state == "22.7" assert hass.states.get(f"{prefix}humidity").state == "63.2" diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index d46284f5049..69dc57b1d2c 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -115,7 +115,7 @@ async def test_user(hass, service): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -127,7 +127,7 @@ async def test_user(hass, service): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -142,7 +142,7 @@ async def test_user_connect_error(hass, service_failed): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -154,7 +154,7 @@ async def test_user_connect_error(hass, service_failed): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "config"} @@ -164,7 +164,7 @@ async def test_user_incomplete_info(hass, service_incomplete): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -176,7 +176,7 @@ async def test_user_incomplete_info(hass, service_incomplete): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE_INCOMPLETE assert result["data"].get(CONF_HOST) == HOST @@ -198,14 +198,14 @@ async def test_abort_if_already_setup(hass, service): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -231,7 +231,7 @@ async def test_ssdp_already_configured(hass): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -257,7 +257,7 @@ async def test_ssdp_ipv6(hass): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_ipv4_address" @@ -277,13 +277,13 @@ async def test_ssdp(hass, service): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -309,13 +309,13 @@ async def test_ssdp_port_5555(hass, service_5555): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -340,7 +340,7 @@ async def test_options_flow(hass, service): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -350,7 +350,7 @@ async def test_options_flow(hass, service): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 1800, } diff --git a/tests/components/network/conftest.py b/tests/components/network/conftest.py index 9c1bf232d7b..8b1b383ae42 100644 --- a/tests/components/network/conftest.py +++ b/tests/components/network/conftest.py @@ -6,4 +6,8 @@ import pytest @pytest.fixture(autouse=True) def mock_get_source_ip(): """Override mock of network util's async_get_source_ip.""" - return + + +@pytest.fixture(autouse=True) +def mock_network(): + """Override mock of network util's async_get_adapters.""" diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py new file mode 100644 index 00000000000..3b513b811b8 --- /dev/null +++ b/tests/components/nextdns/__init__.py @@ -0,0 +1,94 @@ +"""Tests for the NextDNS integration.""" +from unittest.mock import patch + +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, + Settings, +) + +from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +PROFILES = [{"id": "xyz12", "fingerprint": "aabbccdd123", "name": "Fake Profile"}] +STATUS = AnalyticsStatus( + default_queries=40, allowed_queries=30, blocked_queries=20, relayed_queries=10 +) +DNSSEC = AnalyticsDnssec(not_validated_queries=25, validated_queries=75) +ENCRYPTION = AnalyticsEncryption(encrypted_queries=60, unencrypted_queries=40) +IP_VERSIONS = AnalyticsIpVersions(ipv4_queries=90, ipv6_queries=10) +PROTOCOLS = AnalyticsProtocols( + doh_queries=20, + doq_queries=10, + dot_queries=30, + tcp_queries=0, + udp_queries=40, +) +SETTINGS = Settings( + ai_threat_detection=True, + allow_affiliate=True, + anonymized_ecs=True, + block_bypass_methods=True, + block_csam=True, + block_ddns=True, + block_disguised_trackers=True, + block_nrd=True, + block_page=False, + block_parked_domains=True, + cache_boost=True, + cname_flattening=True, + cryptojacking_protection=True, + dga_protection=True, + dns_rebinding_protection=True, + google_safe_browsing=False, + idn_homograph_attacks_protection=True, + logs=True, + safesearch=False, + threat_intelligence_feeds=True, + typosquatting_protection=True, + web3=True, + youtube_restricted_mode=False, +) + + +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the NextDNS integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Fake Profile", + unique_id="xyz12", + data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + ) + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_status", + return_value=STATUS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_encryption", + return_value=ENCRYPTION, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", + return_value=DNSSEC, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", + return_value=IP_VERSIONS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_protocols", + return_value=PROTOCOLS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_settings", + return_value=SETTINGS, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py new file mode 100644 index 00000000000..fabf87f6462 --- /dev/null +++ b/tests/components/nextdns/test_button.py @@ -0,0 +1,48 @@ +"""Test button of NextDNS integration.""" +from unittest.mock import patch + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from . import init_integration + + +async def test_button(hass: HomeAssistant) -> None: + """Test states of the button.""" + registry = er.async_get(hass) + + await init_integration(hass) + + state = hass.states.get("button.fake_profile_clear_logs") + assert state + assert state.state == STATE_UNKNOWN + + entry = registry.async_get("button.fake_profile_clear_logs") + assert entry + assert entry.unique_id == "xyz12_clear_logs" + + +async def test_button_press(hass: HomeAssistant) -> None: + """Test button press.""" + await init_integration(hass) + + now = dt_util.utcnow() + with patch( + "homeassistant.components.nextdns.NextDns.clear_logs" + ) as mock_clear_logs, patch("homeassistant.core.dt_util.utcnow", return_value=now): + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {ATTR_ENTITY_ID: "button.fake_profile_clear_logs"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_clear_logs.assert_called_once() + + state = hass.states.get("button.fake_profile_clear_logs") + assert state + assert state.state == now.isoformat() diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py new file mode 100644 index 00000000000..5f387fd1f64 --- /dev/null +++ b/tests/components/nextdns/test_config_flow.py @@ -0,0 +1,101 @@ +"""Define tests for the NextDNS config flow.""" +import asyncio +from unittest.mock import patch + +from nextdns import ApiError, InvalidApiKeyError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.nextdns.const import ( + CONF_PROFILE_ID, + CONF_PROFILE_NAME, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant + +from . import PROFILES, init_integration + + +async def test_form_create_entry(hass: HomeAssistant) -> None: + """Test that the user step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES + ), patch( + "homeassistant.components.nextdns.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "fake_api_key"}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "profiles" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Fake Profile" + assert result["data"][CONF_API_KEY] == "fake_api_key" + assert result["data"][CONF_PROFILE_ID] == "xyz12" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "exc,base_error", + [ + (ApiError("API Error"), "cannot_connect"), + (InvalidApiKeyError, "invalid_api_key"), + (asyncio.TimeoutError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_errors( + hass: HomeAssistant, exc: Exception, base_error: str +) -> None: + """Test we handle errors.""" + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", side_effect=exc + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_API_KEY: "fake_api_key"}, + ) + + assert result["errors"] == {"base": base_error} + + +async def test_form_already_configured(hass: HomeAssistant) -> None: + """Test that errors are shown when duplicates are added.""" + await init_integration(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES + ): + await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "fake_api_key"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py new file mode 100644 index 00000000000..85dbceafff9 --- /dev/null +++ b/tests/components/nextdns/test_diagnostics.py @@ -0,0 +1,68 @@ +"""Test NextDNS diagnostics.""" +from collections.abc import Awaitable, Callable + +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.components.nextdns import init_integration + + +async def test_entry_diagnostics( + hass: HomeAssistant, hass_client: Callable[..., Awaitable[ClientSession]] +) -> None: + """Test config entry diagnostics.""" + entry = await init_integration(hass) + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert result["config_entry"] == { + "entry_id": entry.entry_id, + "version": 1, + "domain": "nextdns", + "title": "Fake Profile", + "data": {"profile_id": REDACTED, "api_key": REDACTED}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, + } + assert result["dnssec_coordinator_data"] == { + "not_validated_queries": 25, + "validated_queries": 75, + "validated_queries_ratio": 75.0, + } + assert result["encryption_coordinator_data"] == { + "encrypted_queries": 60, + "unencrypted_queries": 40, + "encrypted_queries_ratio": 60.0, + } + assert result["ip_versions_coordinator_data"] == { + "ipv6_queries": 10, + "ipv4_queries": 90, + "ipv6_queries_ratio": 10.0, + } + assert result["protocols_coordinator_data"] == { + "doh_queries": 20, + "doq_queries": 10, + "dot_queries": 30, + "tcp_queries": 0, + "udp_queries": 40, + "doh_queries_ratio": 20.0, + "doq_queries_ratio": 10.0, + "dot_queries_ratio": 30.0, + "tcp_queries_ratio": 0.0, + "udp_queries_ratio": 40.0, + } + assert result["status_coordinator_data"] == { + "all_queries": 100, + "allowed_queries": 30, + "blocked_queries": 20, + "default_queries": 40, + "relayed_queries": 10, + "blocked_queries_ratio": 20.0, + } diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py new file mode 100644 index 00000000000..fb9ea74509e --- /dev/null +++ b/tests/components/nextdns/test_init.py @@ -0,0 +1,55 @@ +"""Test init of NextDNS integration.""" +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_API_KEY, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import MockConfigEntry + + +async def test_async_setup_entry(hass: HomeAssistant) -> None: + """Test a successful setup entry.""" + await init_integration(hass) + + state = hass.states.get("sensor.fake_profile_dns_queries_blocked_ratio") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "20.0" + + +async def test_config_not_ready(hass: HomeAssistant) -> None: + """Test for setup failure if the connection to the service fails.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Fake Profile", + unique_id="xyz12", + data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + ) + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", + side_effect=ApiError("API Error"), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py new file mode 100644 index 00000000000..a90999a592b --- /dev/null +++ b/tests/components/nextdns/test_sensor.py @@ -0,0 +1,537 @@ +"""Test sensor of NextDNS integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorStateClass, +) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import DNSSEC, ENCRYPTION, IP_VERSIONS, PROTOCOLS, STATUS, init_integration + +from tests.common import async_fire_time_changed + + +async def test_sensor(hass: HomeAssistant) -> None: + """Test states of sensors.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh_queries", + suggested_object_id="fake_profile_dns_over_https_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh_queries_ratio", + suggested_object_id="fake_profile_dns_over_https_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doq_queries", + suggested_object_id="fake_profile_dns_over_quic_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doq_queries_ratio", + suggested_object_id="fake_profile_dns_over_quic_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_dot_queries", + suggested_object_id="fake_profile_dns_over_tls_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_dot_queries_ratio", + suggested_object_id="fake_profile_dns_over_tls_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_not_validated_queries", + suggested_object_id="fake_profile_dnssec_not_validated_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_validated_queries", + suggested_object_id="fake_profile_dnssec_validated_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_validated_queries_ratio", + suggested_object_id="fake_profile_dnssec_validated_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_encrypted_queries", + suggested_object_id="fake_profile_encrypted_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_encrypted_queries_ratio", + suggested_object_id="fake_profile_encrypted_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv4_queries", + suggested_object_id="fake_profile_ipv4_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv6_queries", + suggested_object_id="fake_profile_ipv6_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv6_queries_ratio", + suggested_object_id="fake_profile_ipv6_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_tcp_queries", + suggested_object_id="fake_profile_tcp_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_tcp_queries_ratio", + suggested_object_id="fake_profile_tcp_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_udp_queries", + suggested_object_id="fake_profile_udp_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_udp_queries_ratio", + suggested_object_id="fake_profile_udp_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_unencrypted_queries", + suggested_object_id="fake_profile_unencrypted_queries", + disabled_by=None, + ) + + await init_integration(hass) + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state == "100" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_queries") + assert entry + assert entry.unique_id == "xyz12_all_queries" + + state = hass.states.get("sensor.fake_profile_dns_queries_blocked") + assert state + assert state.state == "20" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_queries_blocked") + assert entry + assert entry.unique_id == "xyz12_blocked_queries" + + state = hass.states.get("sensor.fake_profile_dns_queries_blocked_ratio") + assert state + assert state.state == "20.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_queries_blocked_ratio") + assert entry + assert entry.unique_id == "xyz12_blocked_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dns_queries_relayed") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_queries_relayed") + assert entry + assert entry.unique_id == "xyz12_relayed_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state == "20" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_https_queries") + assert entry + assert entry.unique_id == "xyz12_doh_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries_ratio") + assert state + assert state.state == "20.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_https_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_doh_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dns_over_quic_queries") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries") + assert entry + assert entry.unique_id == "xyz12_doq_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_quic_queries_ratio") + assert state + assert state.state == "10.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_doq_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dns_over_tls_queries") + assert state + assert state.state == "30" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries") + assert entry + assert entry.unique_id == "xyz12_dot_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_tls_queries_ratio") + assert state + assert state.state == "30.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_dot_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dnssec_not_validated_queries") + assert state + assert state.state == "25" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dnssec_not_validated_queries") + assert entry + assert entry.unique_id == "xyz12_not_validated_queries" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state == "75" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries") + assert entry + assert entry.unique_id == "xyz12_validated_queries" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries_ratio") + assert state + assert state.state == "75.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_validated_queries_ratio" + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state == "60" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_encrypted_queries") + assert entry + assert entry.unique_id == "xyz12_encrypted_queries" + + state = hass.states.get("sensor.fake_profile_unencrypted_queries") + assert state + assert state.state == "40" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_unencrypted_queries") + assert entry + assert entry.unique_id == "xyz12_unencrypted_queries" + + state = hass.states.get("sensor.fake_profile_encrypted_queries_ratio") + assert state + assert state.state == "60.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_encrypted_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_encrypted_queries_ratio" + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state == "90" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_ipv4_queries") + assert entry + assert entry.unique_id == "xyz12_ipv4_queries" + + state = hass.states.get("sensor.fake_profile_ipv6_queries") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_ipv6_queries") + assert entry + assert entry.unique_id == "xyz12_ipv6_queries" + + state = hass.states.get("sensor.fake_profile_ipv6_queries_ratio") + assert state + assert state.state == "10.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_ipv6_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_ipv6_queries_ratio" + + state = hass.states.get("sensor.fake_profile_tcp_queries") + assert state + assert state.state == "0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_tcp_queries") + assert entry + assert entry.unique_id == "xyz12_tcp_queries" + + state = hass.states.get("sensor.fake_profile_tcp_queries_ratio") + assert state + assert state.state == "0.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_tcp_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_tcp_queries_ratio" + + state = hass.states.get("sensor.fake_profile_udp_queries") + assert state + assert state.state == "40" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_udp_queries") + assert entry + assert entry.unique_id == "xyz12_udp_queries" + + state = hass.states.get("sensor.fake_profile_udp_queries_ratio") + assert state + assert state.state == "40.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_udp_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_udp_queries_ratio" + + +async def test_availability(hass: HomeAssistant) -> None: + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh_queries", + suggested_object_id="fake_profile_dns_over_https_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_validated_queries", + suggested_object_id="fake_profile_dnssec_validated_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_encrypted_queries", + suggested_object_id="fake_profile_encrypted_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv4_queries", + suggested_object_id="fake_profile_ipv4_queries", + disabled_by=None, + ) + + await init_integration(hass) + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "100" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "20" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "75" + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "60" + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "90" + + future = utcnow() + timedelta(minutes=10) + with patch( + "homeassistant.components.nextdns.NextDns.get_analytics_status", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_encryption", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_protocols", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=20) + with patch( + "homeassistant.components.nextdns.NextDns.get_analytics_status", + return_value=STATUS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_encryption", + return_value=ENCRYPTION, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", + return_value=DNSSEC, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", + return_value=IP_VERSIONS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_protocols", + return_value=PROTOCOLS, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "100" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "20" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "75" + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "60" + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "90" diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py new file mode 100644 index 00000000000..3e07a2633d1 --- /dev/null +++ b/tests/components/nextdns/test_switch.py @@ -0,0 +1,306 @@ +"""Test switch of NextDNS integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import SETTINGS, init_integration + +from tests.common import async_fire_time_changed + + +async def test_switch(hass: HomeAssistant) -> None: + """Test states of the switches.""" + registry = er.async_get(hass) + + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_ai_driven_threat_detection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_ai_driven_threat_detection") + assert entry + assert entry.unique_id == "xyz12_ai_threat_detection" + + state = hass.states.get("switch.fake_profile_allow_affiliate_tracking_links") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_allow_affiliate_tracking_links") + assert entry + assert entry.unique_id == "xyz12_allow_affiliate" + + state = hass.states.get("switch.fake_profile_anonymized_edns_client_subnet") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_anonymized_edns_client_subnet") + assert entry + assert entry.unique_id == "xyz12_anonymized_ecs" + + state = hass.states.get("switch.fake_profile_block_bypass_methods") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_bypass_methods") + assert entry + assert entry.unique_id == "xyz12_block_bypass_methods" + + state = hass.states.get("switch.fake_profile_block_child_sexual_abuse_material") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_child_sexual_abuse_material") + assert entry + assert entry.unique_id == "xyz12_block_csam" + + state = hass.states.get("switch.fake_profile_block_disguised_third_party_trackers") + assert state + assert state.state == STATE_ON + + entry = registry.async_get( + "switch.fake_profile_block_disguised_third_party_trackers" + ) + assert entry + assert entry.unique_id == "xyz12_block_disguised_trackers" + + state = hass.states.get("switch.fake_profile_block_dynamic_dns_hostnames") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_dynamic_dns_hostnames") + assert entry + assert entry.unique_id == "xyz12_block_ddns" + + state = hass.states.get("switch.fake_profile_block_newly_registered_domains") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_newly_registered_domains") + assert entry + assert entry.unique_id == "xyz12_block_nrd" + + state = hass.states.get("switch.fake_profile_block_page") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_block_page") + assert entry + assert entry.unique_id == "xyz12_block_page" + + state = hass.states.get("switch.fake_profile_block_parked_domains") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_parked_domains") + assert entry + assert entry.unique_id == "xyz12_block_parked_domains" + + state = hass.states.get("switch.fake_profile_cname_flattening") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_cname_flattening") + assert entry + assert entry.unique_id == "xyz12_cname_flattening" + + state = hass.states.get("switch.fake_profile_cache_boost") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_cache_boost") + assert entry + assert entry.unique_id == "xyz12_cache_boost" + + state = hass.states.get("switch.fake_profile_cryptojacking_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_cryptojacking_protection") + assert entry + assert entry.unique_id == "xyz12_cryptojacking_protection" + + state = hass.states.get("switch.fake_profile_dns_rebinding_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_dns_rebinding_protection") + assert entry + assert entry.unique_id == "xyz12_dns_rebinding_protection" + + state = hass.states.get( + "switch.fake_profile_domain_generation_algorithms_protection" + ) + assert state + assert state.state == STATE_ON + + entry = registry.async_get( + "switch.fake_profile_domain_generation_algorithms_protection" + ) + assert entry + assert entry.unique_id == "xyz12_dga_protection" + + state = hass.states.get("switch.fake_profile_force_safesearch") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_force_safesearch") + assert entry + assert entry.unique_id == "xyz12_safesearch" + + state = hass.states.get("switch.fake_profile_force_youtube_restricted_mode") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_force_youtube_restricted_mode") + assert entry + assert entry.unique_id == "xyz12_youtube_restricted_mode" + + state = hass.states.get("switch.fake_profile_google_safe_browsing") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_google_safe_browsing") + assert entry + assert entry.unique_id == "xyz12_google_safe_browsing" + + state = hass.states.get("switch.fake_profile_idn_homograph_attacks_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_idn_homograph_attacks_protection") + assert entry + assert entry.unique_id == "xyz12_idn_homograph_attacks_protection" + + state = hass.states.get("switch.fake_profile_logs") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_logs") + assert entry + assert entry.unique_id == "xyz12_logs" + + state = hass.states.get("switch.fake_profile_threat_intelligence_feeds") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_threat_intelligence_feeds") + assert entry + assert entry.unique_id == "xyz12_threat_intelligence_feeds" + + state = hass.states.get("switch.fake_profile_typosquatting_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_typosquatting_protection") + assert entry + assert entry.unique_id == "xyz12_typosquatting_protection" + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_web3") + assert entry + assert entry.unique_id == "xyz12_web3" + + +async def test_switch_on(hass: HomeAssistant) -> None: + """Test the switch can be turned on.""" + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_block_page") + assert state + assert state.state == STATE_OFF + + with patch( + "homeassistant.components.nextdns.NextDns.set_setting", return_value=True + ) as mock_switch_on: + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.fake_profile_block_page"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_block_page") + assert state + assert state.state == STATE_ON + + mock_switch_on.assert_called_once() + + +async def test_switch_off(hass: HomeAssistant) -> None: + """Test the switch can be turned on.""" + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_ON + + with patch( + "homeassistant.components.nextdns.NextDns.set_setting", return_value=True + ) as mock_switch_on: + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.fake_profile_web3"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_OFF + + mock_switch_on.assert_called_once() + + +async def test_availability(hass: HomeAssistant) -> None: + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON + + future = utcnow() + timedelta(minutes=10) + with patch( + "homeassistant.components.nextdns.NextDns.get_settings", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=20) + with patch( + "homeassistant.components.nextdns.NextDns.get_settings", + return_value=SETTINGS, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON diff --git a/tests/components/nextdns/test_system_health.py b/tests/components/nextdns/test_system_health.py new file mode 100644 index 00000000000..14d447947c1 --- /dev/null +++ b/tests/components/nextdns/test_system_health.py @@ -0,0 +1,46 @@ +"""Test NextDNS system health.""" +import asyncio + +from aiohttp import ClientError +from nextdns.const import API_ENDPOINT + +from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import get_system_health_info +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_nextdns_system_health( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test NextDNS system health.""" + aioclient_mock.get(API_ENDPOINT, text="") + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": "ok"} + + +async def test_nextdns_system_health_fail( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test NextDNS system health.""" + aioclient_mock.get(API_ENDPOINT, exc=ClientError) + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": {"type": "failed", "error": "unreachable"}} diff --git a/tests/components/nfandroidtv/test_config_flow.py b/tests/components/nfandroidtv/test_config_flow.py index a8b7b5fef53..cfafced5202 100644 --- a/tests/components/nfandroidtv/test_config_flow.py +++ b/tests/components/nfandroidtv/test_config_flow.py @@ -37,7 +37,7 @@ async def test_flow_user(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -62,7 +62,7 @@ async def test_flow_user_already_configured(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -76,7 +76,7 @@ async def test_flow_user_cannot_connect(hass): context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -91,6 +91,6 @@ async def test_flow_user_unknown_error(hass): context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index d7a54ba28fb..9a2b070c20c 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with _patch_glucose_readings(), _patch_server_status(), _patch_async_setup_entry() as mock_setup_entry: @@ -34,7 +34,7 @@ async def test_form(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == SERVER_STATUS.name # pylint: disable=maybe-no-member assert result2["data"] == CONFIG await hass.async_block_till_done() @@ -56,7 +56,7 @@ async def test_user_form_cannot_connect(hass): {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -78,7 +78,7 @@ async def test_user_form_api_key_required(hass): {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -97,7 +97,7 @@ async def test_user_form_unexpected_exception(hass): {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -112,7 +112,7 @@ async def test_user_form_duplicate(hass): context={"source": config_entries.SOURCE_USER}, data=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index 1578991ba11..a93cecdd102 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -58,7 +58,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -73,7 +73,7 @@ async def test_step_user_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -88,7 +88,7 @@ async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_step_user(hass: HomeAssistant) -> None: @@ -105,7 +105,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "NINA" @@ -120,7 +120,7 @@ async def test_step_user_no_selection(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "no_selection"} @@ -139,7 +139,7 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -163,9 +163,12 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -180,7 +183,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] is None assert dict(config_entry.data) == { @@ -213,10 +216,12 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -231,7 +236,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "no_selection"} @@ -248,11 +253,16 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: with patch( "pynina.baseApi.BaseAPI._makeRequest", side_effect=ApiError("Could not connect to Api"), + ), patch( + "homeassistant.components.nina.async_setup_entry", + return_value=True, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -268,11 +278,16 @@ async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: with patch( "pynina.baseApi.BaseAPI._makeRequest", side_effect=Exception("DUMMY"), + ), patch( + "homeassistant.components.nina.async_setup_entry", + return_value=True, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: @@ -306,7 +321,7 @@ async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entity_registry: er = er.async_get(hass) entries = er.async_entries_for_config_entry( diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 3016727f7be..9a94efb6968 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -202,7 +202,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { @@ -231,7 +231,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", CONF_HOME_INTERVAL: 5, diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 45bcedde155..92e285ba899 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -15,7 +15,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -45,7 +45,7 @@ async def test_step_reauth(hass, config, config_entry, setup_notion): assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch("homeassistant.components.notion.async_setup_entry", return_value=True): @@ -54,7 +54,7 @@ async def test_step_reauth(hass, config, config_entry, setup_notion): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -64,7 +64,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -73,7 +73,7 @@ async def test_step_user(hass, config, setup_notion): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@host.com" assert result["data"] == { CONF_USERNAME: "user@host.com", diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index 77966bd7e5f..72713e24e99 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -38,7 +38,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -67,7 +67,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -90,7 +90,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -113,7 +113,7 @@ async def test_form_unknown_exception(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -137,7 +137,7 @@ async def test_form_already_configured(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_dhcp_flow(hass): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -168,7 +168,7 @@ async def test_dhcp_flow(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -189,7 +189,7 @@ async def test_dhcp_flow_already_configured(hass): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -200,7 +200,7 @@ async def test_reauth_success(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -216,7 +216,7 @@ async def test_reauth_success(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data[CONF_TOKEN] == "new-token" @@ -228,7 +228,7 @@ async def test_reauth_invalid_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -240,7 +240,7 @@ async def test_reauth_invalid_auth(hass): user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "invalid_auth"} @@ -252,7 +252,7 @@ async def test_reauth_cannot_connect(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -264,7 +264,7 @@ async def test_reauth_cannot_connect(hass): user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "cannot_connect"} @@ -276,7 +276,7 @@ async def test_reauth_unknown_exception(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -288,6 +288,6 @@ async def test_reauth_unknown_exception(hass): user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 9921d2a639e..8d7f8a91ae8 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -22,6 +22,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM @@ -689,3 +690,161 @@ async def test_restore_number_restore_state( assert entity0.native_value == native_value assert type(entity0.native_value) == native_value_type assert entity0.native_unit_of_measurement == uom + + +@pytest.mark.parametrize( + "device_class,native_unit,custom_unit,state_unit,native_value,custom_value", + [ + # Not a supported temperature unit + ( + NumberDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + "my_temperature_unit", + TEMP_CELSIUS, + 1000, + 1000, + ), + ( + NumberDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + 37.5, + 99.5, + ), + ( + NumberDeviceClass.TEMPERATURE, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + TEMP_CELSIUS, + 100, + 38.0, + ), + ], +) +async def test_custom_unit( + hass, + enable_custom_integrations, + device_class, + native_unit, + custom_unit, + state_unit, + native_value, + custom_value, +): + """Test custom unit.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("number", "test", "very_unique") + entity_registry.async_update_entity_options( + entry.entity_id, "number", {"unit_of_measurement": custom_unit} + ) + await hass.async_block_till_done() + + platform = getattr(hass.components, "test.number") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockNumberEntity( + name="Test", + native_value=native_value, + native_unit_of_measurement=native_unit, + device_class=device_class, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(custom_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit + + +@pytest.mark.parametrize( + "native_unit, custom_unit, used_custom_unit, default_unit, native_value, custom_value, default_value", + [ + ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + 37.5, + 99.5, + 37.5, + ), + ( + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + 100, + 100, + 38.0, + ), + # Not a supported temperature unit + (TEMP_CELSIUS, "no_unit", TEMP_CELSIUS, TEMP_CELSIUS, 1000, 1000, 1000), + ], +) +async def test_custom_unit_change( + hass, + enable_custom_integrations, + native_unit, + custom_unit, + used_custom_unit, + default_unit, + native_value, + custom_value, + default_value, +): + """Test custom unit changes are picked up.""" + entity_registry = er.async_get(hass) + platform = getattr(hass.components, "test.number") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockNumberEntity( + name="Test", + native_value=native_value, + native_unit_of_measurement=native_unit, + device_class=NumberDeviceClass.TEMPERATURE, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + # Default unit conversion according to unit system + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(default_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == default_unit + + entity_registry.async_update_entity_options( + "number.test", "number", {"unit_of_measurement": custom_unit} + ) + await hass.async_block_till_done() + + # Unit conversion to the custom unit + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(custom_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == used_custom_unit + + entity_registry.async_update_entity_options( + "number.test", "number", {"unit_of_measurement": native_unit} + ) + await hass.async_block_till_done() + + # Unit conversion to another custom unit + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(native_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + entity_registry.async_update_entity_options("number.test", "number", None) + await hass.async_block_till_done() + + # Default unit conversion according to unit system + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(default_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == default_unit diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 2261fec3a86..2bb6a5d8286 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form_zeroconf(hass): type="mock_type", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -66,7 +66,7 @@ async def test_form_zeroconf(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "192.168.1.5:1234" assert result2["data"] == { CONF_HOST: "192.168.1.5", @@ -84,7 +84,7 @@ async def test_form_user_one_ups(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -109,7 +109,7 @@ async def test_form_user_one_ups(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -133,7 +133,7 @@ async def test_form_user_multiple_ups(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -156,7 +156,7 @@ async def test_form_user_multiple_ups(hass): ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -171,7 +171,7 @@ async def test_form_user_multiple_ups(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "ups2@1.1.1.1:2222" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -194,7 +194,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -219,7 +219,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -252,7 +252,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch( @@ -272,7 +272,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch( @@ -292,7 +292,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -329,7 +329,7 @@ async def test_abort_if_already_setup(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -368,7 +368,7 @@ async def test_abort_if_already_setup_alias(hass): ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -379,7 +379,7 @@ async def test_abort_if_already_setup_alias(hass): {CONF_ALIAS: "ups1"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -396,14 +396,14 @@ async def test_options_flow(hass): with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 60, } @@ -411,7 +411,7 @@ async def test_options_flow(hass): with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result2 = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -419,7 +419,7 @@ async def test_options_flow(hass): user_input={CONF_SCAN_INTERVAL: 12}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 12, } diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index 8799e3adcf0..fd28f6efa41 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -6,11 +6,7 @@ from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SCAN_INTERVAL, CONF_VERIFY_SSL -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( ENTRY_CONFIG, @@ -30,7 +26,7 @@ async def test_user_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry() as mock_setup_entry: @@ -40,7 +36,7 @@ async def test_user_form(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: False} @@ -53,7 +49,7 @@ async def test_user_form_show_advanced_options(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} user_input_advanced = { @@ -68,7 +64,7 @@ async def test_user_form_show_advanced_options(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: True} @@ -90,7 +86,7 @@ async def test_user_form_cannot_connect(hass): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -109,7 +105,7 @@ async def test_user_form_unexpected_exception(hass): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -123,7 +119,7 @@ async def test_user_form_single_instance_allowed(hass): context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -143,7 +139,7 @@ async def test_options_flow(hass, nzbget_api): assert entry.options[CONF_SCAN_INTERVAL] == 5 result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with _patch_async_setup_entry(): @@ -153,5 +149,5 @@ async def test_options_flow(hass, nzbget_api): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 15 diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index 7769e082016..e9de98206d1 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -230,7 +230,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_show_ssdp_form(hass: HomeAssistant) -> None: @@ -296,7 +296,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_import_yaml(hass: HomeAssistant) -> None: @@ -328,7 +328,7 @@ async def test_import_yaml(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert "errors" not in result @@ -362,7 +362,7 @@ async def test_import_duplicate_yaml(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(request_app_key.mock_calls) == 0 - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index bd8f32b05bd..f3a60479b46 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -129,7 +129,7 @@ async def test_option_flow(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -137,6 +137,6 @@ async def test_option_flow(hass): user_input={"polling_interval": 9}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"]["polling_interval"] == 9 diff --git a/tests/components/oncue/test_config_flow.py b/tests/components/oncue/test_config_flow.py index df9de02a6b3..718a3b08adb 100644 --- a/tests/components/oncue/test_config_flow.py +++ b/tests/components/oncue/test_config_flow.py @@ -7,11 +7,7 @@ from aiooncue import LoginFailedException from homeassistant import config_entries from homeassistant.components.oncue.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch("homeassistant.components.oncue.config_flow.Oncue.async_login"), patch( @@ -37,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "TEST-username", @@ -64,7 +60,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -86,7 +82,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -108,7 +104,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -137,5 +133,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py index e1edfc2a63c..17db65f6b41 100644 --- a/tests/components/ondilo_ico/test_config_flow.py +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -25,7 +25,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index d599c10ea90..57677fc5bff 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.onewire.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture(autouse=True, name="mock_setup_entry") @@ -29,7 +25,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] # Invalid server @@ -42,7 +38,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -55,7 +51,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "1.2.3.4" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -76,7 +72,7 @@ async def test_user_duplicate( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -85,7 +81,7 @@ async def test_user_duplicate( result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/onewire/test_options_flow.py b/tests/components/onewire/test_options_flow.py index e27b5a368d9..795f8a50c99 100644 --- a/tests/components/onewire/test_options_flow.py +++ b/tests/components/onewire/test_options_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.onewire.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import setup_owproxy_mock_devices from .const import MOCK_OWPROXY_DEVICES @@ -49,7 +45,7 @@ async def test_user_options_clear( result["flow_id"], user_input={INPUT_ENTRY_CLEAR_OPTIONS: True}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {} @@ -78,7 +74,7 @@ async def test_user_options_empty_selection( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: []}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "device_selection" assert result["errors"] == {"base": "device_not_selected"} @@ -111,7 +107,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: ["28.111111111111"]}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"]["sensor_id"] == "28.111111111111" # Verify that the setting for the device comes back as default when no input is given @@ -119,7 +115,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.111111111111"]["precision"] == "temperature" @@ -167,7 +163,7 @@ async def test_user_options_set_multiple( ] }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.222222222222)" @@ -178,7 +174,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.111111111111)" @@ -189,7 +185,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature9"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.222222222222"]["precision"] == "temperature" @@ -213,5 +209,5 @@ async def test_user_options_no_devices( # Verify that first config step comes back with an empty list of possible devices to choose from result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "No configurable devices found." diff --git a/tests/components/onvif/test_button.py b/tests/components/onvif/test_button.py index a8ac24da524..be418acd1e0 100644 --- a/tests/components/onvif/test_button.py +++ b/tests/components/onvif/test_button.py @@ -38,3 +38,33 @@ async def test_reboot_button_press(hass): await hass.async_block_till_done() devicemgmt.SystemReboot.assert_called_once() + + +async def test_set_dateandtime_button(hass): + """Test states of the SetDateAndTime button.""" + await setup_onvif_integration(hass) + + state = hass.states.get("button.testcamera_set_system_date_and_time") + assert state + assert state.state == STATE_UNKNOWN + + registry = er.async_get(hass) + entry = registry.async_get("button.testcamera_set_system_date_and_time") + assert entry + assert entry.unique_id == f"{MAC}_setsystemdatetime" + + +async def test_set_dateandtime_button_press(hass): + """Test SetDateAndTime button press.""" + _, camera, device = await setup_onvif_integration(hass) + device.async_manually_set_date_and_time = AsyncMock(return_value=True) + + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {ATTR_ENTITY_ID: "button.testcamera_set_system_date_and_time"}, + blocking=True, + ) + await hass.async_block_till_done() + + device.async_manually_set_date_and_time.assert_called_once() diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index fec1e2b8132..31c2f06f352 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -73,7 +73,7 @@ async def test_flow_discovered_devices(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -91,7 +91,7 @@ async def test_flow_discovered_devices(hass): result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device" assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 3 @@ -99,7 +99,7 @@ async def test_flow_discovered_devices(hass): result["flow_id"], user_input={config_flow.CONF_HOST: f"{URN} ({HOST})"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -116,7 +116,7 @@ async def test_flow_discovered_devices(hass): await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{URN} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: URN, @@ -135,7 +135,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -153,7 +153,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device" assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 2 @@ -162,7 +162,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): user_input={config_flow.CONF_HOST: config_flow.CONF_MANUAL_INPUT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" @@ -174,7 +174,7 @@ async def test_flow_discovered_no_device(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -192,7 +192,7 @@ async def test_flow_discovered_no_device(hass): result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" @@ -216,7 +216,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -235,7 +235,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): ) # It should skip to manual entry if the only devices are already configured - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" result = await hass.config_entries.flow.async_configure( @@ -250,7 +250,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): ) # It should abort if already configured and entered manually - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_flow_manual_entry(hass): @@ -259,7 +259,7 @@ async def test_flow_manual_entry(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -279,7 +279,7 @@ async def test_flow_manual_entry(hass): user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -299,7 +299,7 @@ async def test_flow_manual_entry(hass): await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{NAME} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: NAME, @@ -318,7 +318,7 @@ async def test_option_flow(hass): entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "onvif_devices" result = await hass.config_entries.options.async_configure( @@ -330,7 +330,7 @@ async def test_option_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { config_flow.CONF_EXTRA_ARGUMENTS: "", config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py index f985e2a6193..0dd81d35856 100644 --- a/tests/components/open_meteo/test_config_flow.py +++ b/tests/components/open_meteo/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components.zone import ENTITY_ID_HOME from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_ZONE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow( @@ -19,7 +19,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -28,6 +28,6 @@ async def test_full_user_flow( user_input={CONF_ZONE: ENTITY_ID_HOME}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "test home" assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME} diff --git a/tests/components/openalpr_local/__init__.py b/tests/components/openalpr_local/__init__.py deleted file mode 100644 index 36b7703491f..00000000000 --- a/tests/components/openalpr_local/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the openalpr_local component.""" diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py deleted file mode 100644 index fefc42fe2ab..00000000000 --- a/tests/components/openalpr_local/test_image_processing.py +++ /dev/null @@ -1,142 +0,0 @@ -"""The tests for the openalpr local platform.""" -from unittest.mock import MagicMock, PropertyMock, patch - -import pytest - -import homeassistant.components.image_processing as ip -from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.setup import async_setup_component - -from tests.common import assert_setup_component, async_capture_events, load_fixture -from tests.components.image_processing import common - - -@pytest.fixture -async def setup_openalpr_local(hass): - """Set up openalpr local.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera", "name": "test local"}, - "region": "eu", - }, - "camera": {"platform": "demo"}, - } - - with patch( - "homeassistant.components.openalpr_local.image_processing." - "OpenAlprLocalEntity.should_poll", - new_callable=PropertyMock(return_value=False), - ): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - -@pytest.fixture -def url(hass, setup_openalpr_local): - """Return the camera URL.""" - state = hass.states.get("camera.demo_camera") - return f"{hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" - - -@pytest.fixture -async def alpr_events(hass): - """Listen for events.""" - return async_capture_events(hass, "image_processing.found_plate") - - -@pytest.fixture -def popen_mock(): - """Get a Popen mock back.""" - async_popen = MagicMock() - - async def communicate(input=None): - """Communicate mock.""" - fixture = bytes(load_fixture("alpr_stdout.txt"), "utf-8") - return (fixture, None) - - async_popen.communicate = communicate - - with patch("asyncio.create_subprocess_exec", return_value=async_popen) as mock: - yield mock - - -async def test_setup_platform(hass): - """Set up platform with one entity.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera"}, - "region": "eu", - }, - "camera": {"platform": "demo"}, - } - - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - assert hass.states.get("image_processing.openalpr_demo_camera") - - -async def test_setup_platform_name(hass): - """Set up platform with one entity and set name.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera", "name": "test local"}, - "region": "eu", - }, - "camera": {"platform": "demo"}, - } - - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - assert hass.states.get("image_processing.test_local") - - -async def test_setup_platform_without_region(hass): - """Set up platform with one entity without region.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera"}, - }, - "camera": {"platform": "demo"}, - } - - with assert_setup_component(0, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - -async def test_openalpr_process_image( - setup_openalpr_local, - url, - hass, - alpr_events, - popen_mock, - aioclient_mock, -): - """Set up and scan a picture and test plates from event.""" - aioclient_mock.get(url, content=b"image") - - common.async_scan(hass, entity_id="image_processing.test_local") - await hass.async_block_till_done() - - state = hass.states.get("image_processing.test_local") - - assert popen_mock.called - assert len(alpr_events) == 5 - assert state.attributes.get("vehicles") == 1 - assert state.state == "PE3R2X" - - event_data = [ - event.data for event in alpr_events if event.data.get("plate") == "PE3R2X" - ] - assert len(event_data) == 1 - assert event_data[0]["plate"] == "PE3R2X" - assert event_data[0]["confidence"] == float(98.9371) - assert event_data[0]["entity_id"] == "image_processing.test_local" diff --git a/tests/components/opengarage/test_config_flow.py b/tests/components/opengarage/test_config_flow.py index 5406c40b3aa..39cbb9c4b6b 100644 --- a/tests/components/opengarage/test_config_flow.py +++ b/tests/components/opengarage/test_config_flow.py @@ -6,11 +6,7 @@ import aiohttp from homeassistant import config_entries from homeassistant.components.opengarage.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -37,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "http://1.1.1.1", @@ -63,7 +59,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -82,7 +78,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -101,7 +97,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -132,5 +128,5 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 344b21e4471..080e9a96d58 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -164,7 +164,7 @@ async def test_form_connection_timeout(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": "timeout_connect"} assert len(mock_connect.mock_calls) == 1 @@ -220,7 +220,7 @@ async def test_options_migration(hass): entry.entry_id, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -228,7 +228,7 @@ async def test_options_migration(hass): user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_FLOOR_TEMP] is True @@ -259,7 +259,7 @@ async def test_options_form(hass): result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -272,7 +272,7 @@ async def test_options_form(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_HALVES assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -286,7 +286,7 @@ async def test_options_form(hass): result["flow_id"], user_input={CONF_READ_PRECISION: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == 0.0 assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -306,7 +306,7 @@ async def test_options_form(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is False diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 8960d39a5b9..9f51728365b 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -19,7 +19,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -32,7 +32,7 @@ async def test_invalid_api_key(hass, config): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -41,13 +41,13 @@ async def test_options_flow(hass, config_entry): with patch("homeassistant.components.openuv.async_setup_entry", return_value=True): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} @@ -56,13 +56,13 @@ async def test_step_user(hass, config, setup_openuv): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "51.528308, -0.3817765" assert result["data"] == { CONF_API_KEY: "abcde12345", diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 5225aad83cd..12ee849d3d2 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -63,7 +63,7 @@ async def test_form(hass): await hass.async_block_till_done() assert entry.state == ConfigEntryState.NOT_LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -90,14 +90,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_MODE: "daily"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, @@ -109,14 +109,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_MODE: "onecall_daily"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "onecall_daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 967d6cbb8c8..0542f4dc9fc 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -106,7 +106,7 @@ async def test_form_invalid_auth( ) assert result["step_id"] == config_entries.SOURCE_USER - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -131,7 +131,7 @@ async def test_abort_on_duplicate_entry(hass: HomeAssistant) -> None: {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -177,7 +177,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -220,7 +220,7 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -232,7 +232,7 @@ async def test_zeroconf_flow(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -271,7 +271,7 @@ async def test_zeroconf_flow_already_configured(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -295,7 +295,7 @@ async def test_reauth_success(hass): data=mock_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -311,7 +311,7 @@ async def test_reauth_success(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_entry.data["username"] == TEST_EMAIL assert mock_entry.data["password"] == TEST_PASSWORD2 @@ -337,7 +337,7 @@ async def test_reauth_wrong_account(hass): data=mock_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -353,5 +353,5 @@ async def test_reauth_wrong_account(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_wrong_account" diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index a8f0c098aba..5fbd4586d12 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -22,7 +22,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -32,7 +32,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -44,7 +44,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -55,7 +55,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -67,7 +67,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -78,7 +78,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -93,7 +93,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -110,7 +110,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -119,7 +119,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "authorization_error"} @@ -136,7 +136,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -145,7 +145,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "connection_error"} @@ -167,7 +167,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" assert result["errors"] == {"base": "authorization_error"} @@ -184,5 +184,5 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 31a3a327d1c..e6d337f9420 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -64,11 +64,11 @@ async def test_user(hass, webhook_id, secret): flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "OwnTracks" assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID assert result["data"][CONF_SECRET] == SECRET @@ -98,7 +98,7 @@ async def test_abort_if_already_setup(hass): # Should fail, already setup (flow) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -107,7 +107,7 @@ async def test_user_not_supports_encryption(hass, not_supports_encryption): flow = await init_config_flow(hass) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result["description_placeholders"]["secret"] == "Encryption is not supported because nacl is not installed." @@ -165,7 +165,7 @@ async def test_with_cloud_sub(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] assert entry.data["cloudhook"] assert ( @@ -192,5 +192,5 @@ async def test_with_cloud_sub_not_connected(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index f6ce5fe5d9d..42a41789a92 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components.p1_monitor.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow(hass: HomeAssistant) -> None: @@ -16,7 +16,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +33,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_HOST: "example.com", @@ -58,5 +58,5 @@ async def test_api_error(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py index 98f704e3af8..afa158e2dd9 100644 --- a/tests/components/peco/test_config_flow.py +++ b/tests/components/peco/test_config_flow.py @@ -7,7 +7,7 @@ from voluptuous.error import MultipleInvalid from homeassistant import config_entries from homeassistant.components.peco.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -15,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Philadelphia Outage Count" assert result2["data"] == { "county": "PHILADELPHIA", @@ -42,7 +42,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with raises(MultipleInvalid): @@ -57,7 +57,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: second_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert second_result["type"] == RESULT_TYPE_FORM + assert second_result["type"] == FlowResultType.FORM assert second_result["errors"] is None with patch( @@ -72,7 +72,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert second_result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert second_result2["type"] == FlowResultType.CREATE_ENTRY assert second_result2["title"] == "Philadelphia Outage Count" assert second_result2["data"] == { "county": "PHILADELPHIA", diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index ace62195115..c6bade94ea4 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -219,12 +219,12 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_ALLOW_NOTIFY: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_ALLOW_NOTIFY: True} diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 517697b0e8a..bc86922c89f 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -5,11 +5,7 @@ from unittest.mock import patch from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_API_KEY -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_CONFIG_ENTRY, @@ -44,7 +40,7 @@ async def test_flow_import(hass, caplog): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_CONFIG_ENTRY @@ -52,7 +48,7 @@ async def test_flow_import(hass, caplog): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -63,7 +59,7 @@ async def test_flow_import_invalid(hass, caplog): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len([x for x in caplog.records if x.levelno == logging.ERROR]) == 1 @@ -76,7 +72,7 @@ async def test_flow_user(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} _flow_next(hass, result["flow_id"]) @@ -85,7 +81,7 @@ async def test_flow_user(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW_USER, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_key" assert result["errors"] is None _flow_next(hass, result["flow_id"]) @@ -94,7 +90,7 @@ async def test_flow_user(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW_API_KEY, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_CONFIG_ENTRY @@ -104,7 +100,7 @@ async def test_flow_user(hass): context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -116,7 +112,7 @@ async def test_flow_statistics_only(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} _flow_next(hass, result["flow_id"]) @@ -130,7 +126,7 @@ async def test_flow_statistics_only(hass): result["flow_id"], user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == config_entry_data @@ -142,6 +138,6 @@ async def test_flow_user_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index 3ea54cee593..24878a1b701 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -39,7 +39,7 @@ async def test_form(hass, picnic_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -85,7 +85,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -108,7 +108,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -131,7 +131,7 @@ async def test_form_exception(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -158,7 +158,7 @@ async def test_form_already_configured(hass, picnic_api): ) await hass.async_block_till_done() - assert result_configure["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT assert result_configure["reason"] == "already_configured" @@ -177,7 +177,7 @@ async def test_step_reauth(hass, picnic_api): result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["type"] == data_entry_flow.FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -195,7 +195,7 @@ async def test_step_reauth(hass, picnic_api): await hass.async_block_till_done() # Check that the returned flow has type abort because of successful re-authentication - assert result_configure["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT assert result_configure["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -217,7 +217,7 @@ async def test_step_reauth_failed(hass): result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["type"] == data_entry_flow.FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -256,7 +256,7 @@ async def test_step_reauth_different_account(hass, picnic_api): result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["type"] == data_entry_flow.FlowResultType.FORM assert result_init["step_id"] == "user" with patch( diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index ba244138469..f3abd5a6905 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -12,11 +12,7 @@ from homeassistant.components.plaato.const import ( DOMAIN, ) from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -59,7 +55,7 @@ async def test_show_config_form_device_type_airlock(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) == bool @@ -73,7 +69,7 @@ async def test_show_config_form_device_type_keg(hass): data={CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, CONF_DEVICE_NAME: "device_name"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is None @@ -86,7 +82,7 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -97,7 +93,7 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -119,7 +115,7 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "webhook" @@ -130,7 +126,7 @@ async def test_show_config_form_validate_webhook_not_connected(hass, webhook_id) DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -141,7 +137,7 @@ async def test_show_config_form_validate_webhook_not_connected(hass, webhook_id) }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -163,7 +159,7 @@ async def test_show_config_form_validate_webhook_not_connected(hass, webhook_id) }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" @@ -182,7 +178,7 @@ async def test_show_config_form_validate_token(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): @@ -190,7 +186,7 @@ async def test_show_config_form_validate_token(hass): result["flow_id"], user_input={CONF_TOKEN: "valid_token"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == PlaatoDeviceType.Keg.name assert result["data"] == { CONF_USE_WEBHOOK: False, @@ -215,7 +211,7 @@ async def test_show_config_form_no_cloud_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( @@ -226,7 +222,7 @@ async def test_show_config_form_no_cloud_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "webhook" assert result["errors"] is None @@ -247,14 +243,14 @@ async def test_show_config_form_api_method_no_auth_token(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_auth_token" @@ -272,14 +268,14 @@ async def test_show_config_form_api_method_no_auth_token(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_api_method" @@ -304,7 +300,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( @@ -314,7 +310,7 @@ async def test_options(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 10 assert len(mock_setup_entry.mock_calls) == 1 @@ -339,7 +335,7 @@ async def test_options_webhook(hass, webhook_id): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "webhook" assert result["description_placeholders"] == {"webhook_url": ""} @@ -350,7 +346,7 @@ async def test_options_webhook(hass, webhook_id): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json index 60bc4c35668..6fcb841cf3e 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json @@ -13,6 +13,9 @@ "model": "Generic heater", "name": "OpenTherm", "vendor": "Techneco", + "lower_bound": 0.0, + "upper_bound": 100.0, + "resolution": 1.0, "maximum_boiler_temperature": 60.0, "binary_sensors": { "dhw_state": false, diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index f2a2ef3bc43..4dbe1d2615f 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -21,11 +21,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -65,6 +61,34 @@ TEST_DISCOVERY2 = ZeroconfServiceInfo( type="mock_type", ) +TEST_DISCOVERY_ANNA = ZeroconfServiceInfo( + host=TEST_HOST, + addresses=[TEST_HOST], + hostname=f"{TEST_HOSTNAME}.local.", + name="mock_name", + port=DEFAULT_PORT, + properties={ + "product": "smile_thermo", + "version": "1.2.3", + "hostname": f"{TEST_HOSTNAME}.local.", + }, + type="mock_type", +) + +TEST_DISCOVERY_ADAM = ZeroconfServiceInfo( + host=TEST_HOST, + addresses=[TEST_HOST], + hostname=f"{TEST_HOSTNAME2}.local.", + name="mock_name", + port=DEFAULT_PORT, + properties={ + "product": "smile_open_therm", + "version": "1.2.3", + "hostname": f"{TEST_HOSTNAME2}.local.", + }, + type="mock_type", +) + @pytest.fixture(name="mock_smile") def mock_smile(): @@ -88,7 +112,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -102,7 +126,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -136,7 +160,7 @@ async def test_zeroconf_flow( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=discovery, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -147,7 +171,7 @@ async def test_zeroconf_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -172,7 +196,7 @@ async def test_zeroconf_flow_stretch( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY2, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -183,7 +207,7 @@ async def test_zeroconf_flow_stretch( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -224,7 +248,7 @@ async def test_zercoconf_discovery_update_configuration( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "0.0.0.0" @@ -235,7 +259,7 @@ async def test_zercoconf_discovery_update_configuration( data=TEST_DISCOVERY, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "1.1.1.1" @@ -262,7 +286,7 @@ async def test_flow_errors( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -273,7 +297,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": reason} assert result2.get("step_id") == "user" @@ -286,7 +310,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Test Smile Name" assert result3.get("data") == { CONF_HOST: TEST_HOST, @@ -298,3 +322,61 @@ async def test_flow_errors( assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smile_config_flow.connect.mock_calls) == 2 + + +async def test_zeroconf_abort_anna_with_existing_config_entries( + hass: HomeAssistant, + mock_smile_adam: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test we abort Anna discovery with existing config entries.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ANNA, + ) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "anna_with_adam" + + +async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: + """Test we abort Anna discovery when an Adam is also discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ANNA, + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 1 + assert flows_in_progress[0]["context"]["product"] == "smile_thermo" + + # Discover Adam, Anna should be aborted and no longer present + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ADAM, + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "user" + + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 1 + assert flows_in_progress[0]["context"]["product"] == "smile_open_therm" + + # Discover Anna again, Anna should be aborted directly + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ANNA, + ) + assert result3.get("type") == FlowResultType.ABORT + assert result3.get("reason") == "anna_with_adam" + + # Adam should still be there + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 1 + assert flows_in_progress[0]["context"]["product"] == "smile_open_therm" diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py new file mode 100644 index 00000000000..a4e084e5d3a --- /dev/null +++ b/tests/components/plugwise/test_number.py @@ -0,0 +1,40 @@ +"""Tests for the Plugwise Number integration.""" + +from unittest.mock import MagicMock + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_anna_number_entities( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of a number.""" + state = hass.states.get("number.opentherm_maximum_boiler_temperature_setpoint") + assert state + assert float(state.state) == 60.0 + + +async def test_anna_max_boiler_temp_change( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test changing of number entities.""" + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.opentherm_maximum_boiler_temperature_setpoint", + ATTR_VALUE: 65, + }, + blocking=True, + ) + + assert mock_smile_anna.set_max_boiler_temperature.call_count == 1 + mock_smile_anna.set_max_boiler_temperature.assert_called_with(65) diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py new file mode 100644 index 00000000000..7ec5559a608 --- /dev/null +++ b/tests/components/plugwise/test_select.py @@ -0,0 +1,44 @@ +"""Tests for the Plugwise Select integration.""" + +from unittest.mock import MagicMock + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_adam_select_entities( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test a select.""" + + state = hass.states.get("select.zone_lisa_wk_thermostat_schedule") + assert state + assert state.state == "GF7 Woonkamer" + + +async def test_adam_change_select_entity( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test changing of select entities.""" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.zone_lisa_wk_thermostat_schedule", + ATTR_OPTION: "Badkamer Schema", + }, + blocking=True, + ) + + assert mock_smile_adam.set_schedule_state.call_count == 1 + mock_smile_adam.set_schedule_state.assert_called_with( + "c50f167537524366a5af7aa3942feb1e", "Badkamer Schema", "on" + ) diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 93ea18f21b6..e9587592175 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -48,7 +48,7 @@ async def test_abort_if_no_implementation_registered(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_flows" @@ -58,12 +58,12 @@ async def test_abort_if_already_setup(hass): with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -75,18 +75,18 @@ async def test_full_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "https://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["refresh_args"] == { CONF_CLIENT_ID: "id", CONF_CLIENT_SECRET: "secret", @@ -100,7 +100,7 @@ async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-out flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -112,7 +112,7 @@ async def test_wrong_code_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_error" @@ -121,7 +121,7 @@ async def test_not_pick_implementation_if_only_one(hass): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -130,7 +130,7 @@ async def test_abort_if_timeout_generating_auth_url(hass): flow = init_config_flow(hass, side_effect=asyncio.TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -139,7 +139,7 @@ async def test_abort_if_exception_generating_auth_url(hass): flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -148,5 +148,5 @@ async def test_abort_no_code(hass): flow = init_config_flow(hass) result = await flow.async_step_code() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_code" diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index 71fa76df7ab..0710ff1d26d 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -13,7 +13,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -47,7 +47,7 @@ async def test_valid_credentials(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test-email" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 3ef9e9c0fd1..f4dcfd87b8b 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +from homeassistant.data_entry_flow import FlowResultType from .mocks import ( MOCK_GATEWAY_DIN, @@ -299,7 +299,7 @@ async def test_dhcp_discovery_cannot_connect(hass): hostname="00GGX", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -370,7 +370,7 @@ async def test_dhcp_discovery_update_ip_address(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1" @@ -402,7 +402,7 @@ async def test_dhcp_discovery_updates_unique_id(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" assert entry.unique_id == MOCK_GATEWAY_DIN diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 8c850e0807a..cef6b87963a 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.progettihwsw.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -26,7 +22,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -43,7 +39,7 @@ async def test_form(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "relay_modes" assert result2["errors"] == {} @@ -56,7 +52,7 @@ async def test_form(hass): mock_value_step_rm, ) - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["data"] assert result3["data"]["title"] == "1R & 1IN Board" assert result3["data"]["is_old"] is False @@ -80,7 +76,7 @@ async def test_form_cannot_connect(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -107,7 +103,7 @@ async def test_form_existing_entry_exception(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -128,6 +124,6 @@ async def test_form_user_exception(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index 46832146cf8..8a608d4eeba 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -6,7 +6,7 @@ from pytest import mark from homeassistant import config_entries from homeassistant.components.prosegur.config_flow import CannotConnect, InvalidAuth from homeassistant.components.prosegur.const import DOMAIN -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -145,7 +145,7 @@ async def test_reauth_flow(hass): data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} install = MagicMock() @@ -167,7 +167,7 @@ async def test_reauth_flow(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "country": "PT", @@ -223,5 +223,5 @@ async def test_reauth_flow_error(hass, exception, base_error): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == base_error diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 57e65d15be4..346b9718165 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -103,7 +103,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -111,7 +111,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -121,7 +121,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -131,7 +131,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -144,7 +144,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -152,7 +152,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -163,7 +163,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -174,7 +174,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -196,7 +196,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -204,7 +204,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -215,7 +215,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" # Step Link @@ -226,7 +226,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -249,7 +249,7 @@ async def test_port_bind_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): @@ -257,7 +257,7 @@ async def test_port_bind_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason @@ -269,14 +269,14 @@ async def test_duplicate_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -285,7 +285,7 @@ async def test_duplicate_abort(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -299,14 +299,14 @@ async def test_additional_device(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -322,7 +322,7 @@ async def test_additional_device(hass): result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -336,7 +336,7 @@ async def test_0_pin(hass): context={"source": "creds"}, data={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -348,7 +348,7 @@ async def test_0_pin(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" mock_config = MOCK_CONFIG @@ -372,14 +372,14 @@ async def test_no_devices_found_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]): @@ -387,7 +387,7 @@ async def test_no_devices_found_abort(hass): result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -397,14 +397,14 @@ async def test_manual_mode(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input: manual, results in Step Link. @@ -415,7 +415,7 @@ async def test_manual_mode(hass): result["flow_id"], user_input=MOCK_MANUAL ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -425,7 +425,7 @@ async def test_credential_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None): @@ -433,7 +433,7 @@ async def test_credential_abort(hass): result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "credential_error" @@ -443,7 +443,7 @@ async def test_credential_timeout(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout): @@ -451,7 +451,7 @@ async def test_credential_timeout(hass): result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" assert result["errors"] == {"base": "credential_timeout"} @@ -462,14 +462,14 @@ async def test_wrong_pin_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -483,7 +483,7 @@ async def test_wrong_pin_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "login_failed"} @@ -494,14 +494,14 @@ async def test_device_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -515,7 +515,7 @@ async def test_device_connection_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "cannot_connect"} @@ -526,20 +526,20 @@ async def test_manual_mode_no_ip_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"Config Mode": "Manual Entry"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" assert result["errors"] == {CONF_IP_ADDRESS: "no_ipaddress"} diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index a84adba2a70..7cda5b42fa2 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -43,7 +43,7 @@ MOCK_DATA = {CONF_TOKEN: MOCK_CREDS, "devices": [MOCK_DEVICE]} MOCK_FLOW_RESULT = { "version": VERSION, "handler": DOMAIN, - "type": data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + "type": data_entry_flow.FlowResultType.CREATE_ENTRY, "title": "test_ps4", "data": MOCK_DATA, "options": {}, @@ -125,7 +125,7 @@ async def test_creating_entry_sets_up_media_player(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py index 441a5977a2d..d1ed8eeb578 100644 --- a/tests/components/pure_energie/test_config_flow.py +++ b/tests/components/pure_energie/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.pure_energie.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow_implementation( @@ -27,7 +23,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == SOURCE_USER - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result = await hass.config_entries.flow.async_configure( @@ -35,7 +31,7 @@ async def test_full_user_flow_implementation( ) assert result.get("title") == "Pure Energie Meter" - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -67,7 +63,7 @@ async def test_full_zeroconf_flow_implementationn( CONF_NAME: "Pure Energie Meter", } assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( @@ -75,7 +71,7 @@ async def test_full_zeroconf_flow_implementationn( ) assert result2.get("title") == "Pure Energie Meter" - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -94,7 +90,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -119,5 +115,5 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 444a35565f6..9d6162e4d46 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -39,7 +35,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SYSTEM_ID: 12345, @@ -64,7 +60,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -77,7 +73,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -94,7 +90,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SYSTEM_ID: 12345, @@ -120,7 +116,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 @@ -147,7 +143,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -169,7 +165,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -179,7 +175,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -212,7 +208,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -223,7 +219,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -238,7 +234,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("type") == FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -266,7 +262,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -277,6 +273,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/pvoutput/test_sensor.py b/tests/components/pvoutput/test_sensor.py index a194326cd9d..54d1d3e641f 100644 --- a/tests/components/pvoutput/test_sensor.py +++ b/tests/components/pvoutput/test_sensor.py @@ -31,40 +31,46 @@ async def test_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.energy_consumed") - entry = entity_registry.async_get("sensor.energy_consumed") + state = hass.states.get("sensor.frenck_s_solar_farm_energy_consumed") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_consumed") assert entry assert state assert entry.unique_id == "12345_energy_consumption" assert entry.entity_category is None assert state.state == "1000" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Energy consumed" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.energy_generated") - entry = entity_registry.async_get("sensor.energy_generated") + state = hass.states.get("sensor.frenck_s_solar_farm_energy_generated") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_generated") assert entry assert state assert entry.unique_id == "12345_energy_generation" assert entry.entity_category is None assert state.state == "500" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Generated" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Energy generated" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.efficiency") - entry = entity_registry.async_get("sensor.efficiency") + state = hass.states.get("sensor.frenck_s_solar_farm_efficiency") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_efficiency") assert entry assert state assert entry.unique_id == "12345_normalized_output" assert entry.entity_category is None assert state.state == "0.5" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Efficiency" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Efficiency" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -73,54 +79,59 @@ async def test_sensors( assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.power_consumed") - entry = entity_registry.async_get("sensor.power_consumed") + state = hass.states.get("sensor.frenck_s_solar_farm_power_consumed") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_consumed") assert entry assert state assert entry.unique_id == "12345_power_consumption" assert entry.entity_category is None assert state.state == "2500" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Power consumed" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.power_generated") - entry = entity_registry.async_get("sensor.power_generated") + state = hass.states.get("sensor.frenck_s_solar_farm_power_generated") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_generated") assert entry assert state assert entry.unique_id == "12345_power_generation" assert entry.entity_category is None assert state.state == "1500" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Generated" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Power generated" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.temperature") - entry = entity_registry.async_get("sensor.temperature") + state = hass.states.get("sensor.frenck_s_solar_farm_temperature") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_temperature") assert entry assert state assert entry.unique_id == "12345_temperature" assert entry.entity_category is None assert state.state == "20.2" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Temperature" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Temperature" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.voltage") - entry = entity_registry.async_get("sensor.voltage") + state = hass.states.get("sensor.frenck_s_solar_farm_voltage") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_voltage") assert entry assert state assert entry.unique_id == "12345_voltage" assert entry.entity_category is None assert state.state == "220.5" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Voltage" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Voltage" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT assert ATTR_ICON not in state.attributes diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index db1a72147eb..e67aca154c4 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -43,12 +43,12 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.test") @@ -59,11 +59,11 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert pvpc_aioclient_mock.call_count == 1 # Check removal @@ -75,12 +75,12 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.test") @@ -96,7 +96,7 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): config_entry = current_entries[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 02f873c6a4a..fbc850f8fe1 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -48,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -62,7 +62,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result["title"] == f"QNAP {SYSTEM_BOARD_MOCK[API_RESULT][API_PRODUCT]} {SYSTEM_BOARD_MOCK[API_RESULT][API_MAC_ADDR]}" @@ -159,7 +159,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -206,7 +206,7 @@ async def test_dhcp_flow_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -223,7 +223,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -254,7 +254,7 @@ async def test_dhcp_login_error(hass: HomeAssistant): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index b8e4855fea9..107cfa580b7 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -2,10 +2,13 @@ from unittest.mock import patch -from aioqsw.exceptions import QswError +from aioqsw.exceptions import APIError, QswError from homeassistant.components.qnap_qsw.const import DOMAIN -from homeassistant.components.qnap_qsw.coordinator import SCAN_INTERVAL +from homeassistant.components.qnap_qsw.coordinator import ( + DATA_SCAN_INTERVAL, + FW_SCAN_INTERVAL, +) from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow @@ -14,6 +17,7 @@ from .util import ( CONFIG, FIRMWARE_CONDITION_MOCK, FIRMWARE_INFO_MOCK, + FIRMWARE_UPDATE_CHECK_MOCK, SYSTEM_BOARD_MOCK, SYSTEM_SENSOR_MOCK, SYSTEM_TIME_MOCK, @@ -37,6 +41,9 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_info", return_value=FIRMWARE_INFO_MOCK, ) as mock_firmware_info, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", + return_value=FIRMWARE_UPDATE_CHECK_MOCK, + ) as mock_firmware_update_check, patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, ) as mock_system_board, patch( @@ -57,14 +64,16 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.assert_called_once() mock_firmware_info.assert_called_once() + mock_firmware_update_check.assert_called_once() mock_system_board.assert_called_once() mock_system_sensor.assert_called_once() mock_system_time.assert_called_once() - mock_users_verification.assert_not_called() + mock_users_verification.assert_called_once() mock_users_login.assert_called_once() mock_firmware_condition.reset_mock() mock_firmware_info.reset_mock() + mock_firmware_update_check.reset_mock() mock_system_board.reset_mock() mock_system_sensor.reset_mock() mock_system_time.reset_mock() @@ -72,12 +81,28 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_users_login.reset_mock() mock_system_sensor.side_effect = QswError - async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + async_fire_time_changed(hass, utcnow() + DATA_SCAN_INTERVAL) await hass.async_block_till_done() mock_system_sensor.assert_called_once() - mock_users_verification.assert_called_once() + mock_users_verification.assert_called() mock_users_login.assert_not_called() state = hass.states.get("sensor.qsw_m408_4c_temperature") assert state.state == STATE_UNAVAILABLE + + mock_firmware_update_check.side_effect = APIError + async_fire_time_changed(hass, utcnow() + FW_SCAN_INTERVAL) + await hass.async_block_till_done() + + mock_firmware_update_check.assert_called_once() + mock_firmware_update_check.reset_mock() + + mock_firmware_update_check.side_effect = QswError + async_fire_time_changed(hass, utcnow() + FW_SCAN_INTERVAL) + await hass.async_block_till_done() + + mock_firmware_update_check.assert_called_once() + + update = hass.states.get("update.qsw_m408_4c_firmware_update") + assert update.state == STATE_UNAVAILABLE diff --git a/tests/components/qnap_qsw/test_init.py b/tests/components/qnap_qsw/test_init.py index 211cd7ed41d..fedfdd26543 100644 --- a/tests/components/qnap_qsw/test_init.py +++ b/tests/components/qnap_qsw/test_init.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from aioqsw.exceptions import APIError + from homeassistant.components.qnap_qsw.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -11,6 +13,29 @@ from .util import CONFIG from tests.common import MockConfigEntry +async def test_firmware_check_error(hass: HomeAssistant) -> None: + """Test firmware update check error.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="qsw_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.check_firmware", + side_effect=APIError, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.validate", + return_value=None, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.update", + return_value=None, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + async def test_unload_entry(hass: HomeAssistant) -> None: """Test unload.""" @@ -20,6 +45,9 @@ async def test_unload_entry(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.check_firmware", + return_value=None, + ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.validate", return_value=None, ), patch( diff --git a/tests/components/qnap_qsw/test_update.py b/tests/components/qnap_qsw/test_update.py new file mode 100644 index 00000000000..69f4a3d08b4 --- /dev/null +++ b/tests/components/qnap_qsw/test_update.py @@ -0,0 +1,26 @@ +"""The sensor tests for the QNAP QSW platform.""" + +from aioqsw.const import API_RESULT, API_VERSION + +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant + +from .util import FIRMWARE_INFO_MOCK, FIRMWARE_UPDATE_CHECK_MOCK, async_init_integration + + +async def test_qnap_qsw_update(hass: HomeAssistant) -> None: + """Test creation of update entities.""" + + await async_init_integration(hass) + + update = hass.states.get("update.qsw_m408_4c_firmware_update") + assert update is not None + assert update.state == STATE_OFF + assert ( + update.attributes.get("installed_version") + == FIRMWARE_INFO_MOCK[API_RESULT][API_VERSION] + ) + assert ( + update.attributes.get("latest_version") + == FIRMWARE_UPDATE_CHECK_MOCK[API_RESULT][API_VERSION] + ) diff --git a/tests/components/qnap_qsw/util.py b/tests/components/qnap_qsw/util.py index 28e7f7881d5..a057dfbe3ac 100644 --- a/tests/components/qnap_qsw/util.py +++ b/tests/components/qnap_qsw/util.py @@ -12,6 +12,8 @@ from aioqsw.const import ( API_COMMIT_CPSS, API_COMMIT_ISS, API_DATE, + API_DESCRIPTION, + API_DOWNLOAD_URL, API_ERROR_CODE, API_ERROR_MESSAGE, API_FAN1_SPEED, @@ -20,6 +22,7 @@ from aioqsw.const import ( API_MAX_SWITCH_TEMP, API_MESSAGE, API_MODEL, + API_NEWER, API_NUMBER, API_PORT_NUM, API_PRODUCT, @@ -90,6 +93,24 @@ FIRMWARE_INFO_MOCK = { }, } +FIRMWARE_UPDATE_CHECK_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: { + API_VERSION: "1.2.0", + API_NUMBER: "29649", + API_BUILD_NUMBER: "20220128", + API_DATE: "Fri, 28 Jan 2022 01:17:39 +0800", + API_DESCRIPTION: "", + API_DOWNLOAD_URL: [ + "https://download.qnap.com/Storage/Networking/QSW408FW/QSW-M408AC3-FW.v1.2.0_S20220128_29649.img", + "https://eu1.qnap.com/Storage/Networking/QSW408FW/QSW-M408AC3-FW.v1.2.0_S20220128_29649.img", + "https://us1.qnap.com/Storage/Networking/QSW408FW/QSW-M408AC3-FW.v1.2.0_S20220128_29649.img", + ], + API_NEWER: False, + }, +} + SYSTEM_COMMAND_MOCK = { API_ERROR_CODE: 200, API_ERROR_MESSAGE: "OK", @@ -146,6 +167,9 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_info", return_value=FIRMWARE_INFO_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", + return_value=FIRMWARE_UPDATE_CHECK_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, @@ -155,6 +179,9 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_time", return_value=SYSTEM_TIME_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_users_verification", + return_value=USERS_VERIFICATION_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", return_value=USERS_LOGIN_MOCK, diff --git a/tests/components/radio_browser/test_config_flow.py b/tests/components/radio_browser/test_config_flow.py index 8a5a3d9ccce..56ed98f145b 100644 --- a/tests/components/radio_browser/test_config_flow.py +++ b/tests/components/radio_browser/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import AsyncMock from homeassistant.components.radio_browser.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +14,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") is None assert "flow_id" in result @@ -27,7 +23,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Radio Browser" assert result2.get("data") == {} @@ -46,7 +42,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -58,7 +54,7 @@ async def test_onboarding_flow( DOMAIN, context={"source": "onboarding"} ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "Radio Browser" assert result.get("data") == {} diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index 56a361404f8..862b7b30032 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -47,7 +47,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -72,7 +72,7 @@ async def test_form_unknown_error(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -93,7 +93,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {CONF_HOST: "cannot_connect"} @@ -113,7 +113,7 @@ async def test_import(hass): data={CONF_HOST: "1.2.3.4"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "My Name" assert result["data"] == {CONF_HOST: "1.2.3.4"} assert len(mock_setup_entry.mock_calls) == 1 @@ -131,7 +131,7 @@ async def test_import_cannot_connect(hass): data={CONF_HOST: "1.2.3.4"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -153,7 +153,7 @@ async def test_dhcp_can_confirm(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "host": "1.2.3.4", @@ -171,7 +171,7 @@ async def test_dhcp_can_confirm(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -197,7 +197,7 @@ async def test_dhcp_fails_to_connect(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -226,7 +226,7 @@ async def test_dhcp_already_exists(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -243,7 +243,7 @@ async def test_user_unique_id_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -261,5 +261,5 @@ async def test_user_unique_id_already_exists(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/rainforest_eagle/test_config_flow.py b/tests/components/rainforest_eagle/test_config_flow.py index 370dfebee90..d9b66b0feec 100644 --- a/tests/components/rainforest_eagle/test_config_flow.py +++ b/tests/components/rainforest_eagle/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components.rainforest_eagle.const import ( from homeassistant.components.rainforest_eagle.data import CannotConnect, InvalidAuth from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -21,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "abcdef" assert result2["data"] == { CONF_TYPE: TYPE_EAGLE_200, @@ -72,7 +72,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -95,5 +95,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 8b313eb2fb5..deb03d65cb5 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -16,7 +16,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -95,13 +95,13 @@ async def test_options_flow(hass, config, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_ZONE_RUN_TIME: 600} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_ZONE_RUN_TIME: 600} @@ -112,7 +112,7 @@ async def test_show_form(hass): context={"source": config_entries.SOURCE_USER}, data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -123,7 +123,7 @@ async def test_step_user(hass, config, setup_rainmachine): context={"source": config_entries.SOURCE_USER}, data=config, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -158,7 +158,7 @@ async def test_step_homekit_zeroconf_ip_already_exists( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -184,7 +184,7 @@ async def test_step_homekit_zeroconf_ip_change(hass, client, config_entry, sourc ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_IP_ADDRESS] == "192.168.1.2" @@ -213,7 +213,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -231,7 +231,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "12345" assert result2["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -261,7 +261,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass, client): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -281,5 +281,5 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass, client): ), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/raspberry_pi/test_config_flow.py b/tests/components/raspberry_pi/test_config_flow.py index dfad1100cad..68306b3ea9a 100644 --- a/tests/components/raspberry_pi/test_config_flow.py +++ b/tests/components/raspberry_pi/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.raspberry_pi.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Raspberry Pi" assert result["data"] == {} assert result["options"] == {} @@ -53,6 +53,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/rdw/test_binary_sensor.py b/tests/components/rdw/test_binary_sensor.py index abf15d869ce..aea188db773 100644 --- a/tests/components/rdw/test_binary_sensor.py +++ b/tests/components/rdw/test_binary_sensor.py @@ -16,23 +16,23 @@ async def test_vehicle_binary_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("binary_sensor.liability_insured") - entry = entity_registry.async_get("binary_sensor.liability_insured") + state = hass.states.get("binary_sensor.skoda_11zkz3_liability_insured") + entry = entity_registry.async_get("binary_sensor.skoda_11zkz3_liability_insured") assert entry assert state assert entry.unique_id == "11ZKZ3_liability_insured" assert state.state == "off" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Liability Insured" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 Liability insured" assert state.attributes.get(ATTR_ICON) == "mdi:shield-car" assert ATTR_DEVICE_CLASS not in state.attributes - state = hass.states.get("binary_sensor.pending_recall") - entry = entity_registry.async_get("binary_sensor.pending_recall") + state = hass.states.get("binary_sensor.skoda_11zkz3_pending_recall") + entry = entity_registry.async_get("binary_sensor.skoda_11zkz3_pending_recall") assert entry assert state assert entry.unique_id == "11ZKZ3_pending_recall" assert state.state == "off" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending Recall" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 Pending recall" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PROBLEM assert ATTR_ICON not in state.attributes @@ -41,7 +41,7 @@ async def test_vehicle_binary_sensors( assert device_entry assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} assert device_entry.manufacturer == "Skoda" - assert device_entry.name == "Skoda: 11ZKZ3" + assert device_entry.name == "Skoda 11ZKZ3" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "Citigo" assert ( diff --git a/tests/components/rdw/test_config_flow.py b/tests/components/rdw/test_config_flow.py index 20144768abe..0fe40c29dfa 100644 --- a/tests/components/rdw/test_config_flow.py +++ b/tests/components/rdw/test_config_flow.py @@ -7,7 +7,7 @@ from vehicle.exceptions import RDWConnectionError, RDWUnknownLicensePlateError from homeassistant.components.rdw.const import CONF_LICENSE_PLATE, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow( @@ -18,7 +18,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -29,7 +29,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "11-ZKZ-3" assert result2.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -46,7 +46,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -58,7 +58,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "unknown_license_plate"} assert "flow_id" in result2 @@ -71,7 +71,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "11-ZKZ-3" assert result3.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -88,5 +88,5 @@ async def test_connection_error( data={CONF_LICENSE_PLATE: "0001TJ"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/rdw/test_sensor.py b/tests/components/rdw/test_sensor.py index 32d4c368d73..3e7ad7ab89e 100644 --- a/tests/components/rdw/test_sensor.py +++ b/tests/components/rdw/test_sensor.py @@ -21,25 +21,25 @@ async def test_vehicle_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.apk_expiration") - entry = entity_registry.async_get("sensor.apk_expiration") + state = hass.states.get("sensor.skoda_11zkz3_apk_expiration") + entry = entity_registry.async_get("sensor.skoda_11zkz3_apk_expiration") assert entry assert state assert entry.unique_id == "11ZKZ3_apk_expiration" assert state.state == "2022-01-04" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "APK Expiration" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 APK expiration" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert ATTR_ICON not in state.attributes assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.ascription_date") - entry = entity_registry.async_get("sensor.ascription_date") + state = hass.states.get("sensor.skoda_11zkz3_ascription_date") + entry = entity_registry.async_get("sensor.skoda_11zkz3_ascription_date") assert entry assert state assert entry.unique_id == "11ZKZ3_ascription_date" assert state.state == "2021-11-04" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ascription Date" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 Ascription date" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert ATTR_ICON not in state.attributes assert ATTR_STATE_CLASS not in state.attributes @@ -50,7 +50,7 @@ async def test_vehicle_sensors( assert device_entry assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} assert device_entry.manufacturer == "Skoda" - assert device_entry.name == "Skoda: 11ZKZ3" + assert device_entry.name == "Skoda 11ZKZ3" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "Citigo" assert ( diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index 4295f3777d5..ba09a2f6d6b 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -18,7 +18,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -31,7 +31,7 @@ async def test_invalid_place_or_service_id(hass, config): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_place_or_service_id"} @@ -42,13 +42,13 @@ async def test_options_flow(hass, config, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FRIENDLY_NAME: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FRIENDLY_NAME: True} @@ -57,7 +57,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -67,6 +67,6 @@ async def test_step_user(hass, config, setup_recollect_waste): DOMAIN, context={"source": SOURCE_USER}, data=config ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "12345, 12345" assert result["data"] == {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 20df89eca5b..083630c7ea8 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -62,7 +62,7 @@ def wait_recording_done(hass: HomeAssistant) -> None: hass.block_till_done() trigger_db_commit(hass) hass.block_till_done() - hass.data[recorder.DATA_INSTANCE].block_till_done() + recorder.get_instance(hass).block_till_done() hass.block_till_done() @@ -105,8 +105,7 @@ def async_trigger_db_commit(hass: HomeAssistant) -> None: async def async_recorder_block_till_done(hass: HomeAssistant) -> None: """Non blocking version of recorder.block_till_done().""" - instance: recorder.Recorder = hass.data[recorder.DATA_INSTANCE] - await hass.async_add_executor_job(instance.block_till_done) + await hass.async_add_executor_job(recorder.get_instance(hass).block_till_done) def corrupt_db_file(test_db_file): diff --git a/tests/components/recorder/db_schema_25.py b/tests/components/recorder/db_schema_25.py new file mode 100644 index 00000000000..43aa245a761 --- /dev/null +++ b/tests/components/recorder/db_schema_25.py @@ -0,0 +1,673 @@ +"""Models for SQLAlchemy.""" +from __future__ import annotations + +from datetime import datetime, timedelta +import json +import logging +from typing import Any, TypedDict, cast, overload + +from fnvhash import fnv1a_32 +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + String, + Text, + distinct, +) +from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.engine.row import Row +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.components.recorder.const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.typing import UNDEFINED, UndefinedType +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 25 + +_LOGGER = logging.getLogger(__name__) + +DB_TIMEZONE = "+00:00" + +TABLE_EVENTS = "events" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +EMPTY_JSON_OBJECT = "{}" + + +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) + + +class Events(Base): # type: ignore[misc,valid-type] + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) + time_fired = Column(DATETIME_TYPE, index=True) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event( + event: Event, event_data: UndefinedType | None = UNDEFINED + ) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=JSON_DUMP(event.data) if event_data is UNDEFINED else event_data, + origin=str(event.origin.value), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data), + EventOrigin(self.origin), + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class States(Base): # type: ignore[misc,valid-type] + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + attributes_id = Column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + event = relationship("Events", uselist=False) + old_state = relationship("States", remote_side=[state_id]) + state_attributes = relationship("StateAttributes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> States: + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state: State | None = event.data.get("new_state") + dbstate = States(entity_id=entity_id, attributes=None) + + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.state = state.state + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + try: + return State( + self.entity_id, + self.state, + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + json.loads(self.attributes) if self.attributes else {}, + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), + # Join the events table on event_id to get the context instead + # as it will always be there for state_changed events + context=Context(id=None), # type: ignore[arg-type] + validate_entity_id=validate_entity_id, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class StateAttributes(Base): # type: ignore[misc,valid-type] + """State attribute change history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> StateAttributes: + """Create object from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + dbstate = StateAttributes( + shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) + ) + dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) + return dbstate + + @staticmethod + def shared_attrs_from_event( + event: Event, exclude_attrs_by_domain: dict[str, set[str]] + ) -> str: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return "{}" + domain = split_entity_id(state.entity_id)[0] + exclude_attrs = ( + exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS + ) + return JSON_DUMP( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + + @staticmethod + def hash_shared_attrs(shared_attrs: str) -> int: + """Return the hash of json encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json.loads(self.shared_attrs)) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatisticResult(TypedDict): + """Statistic result data class. + + Allows multiple datapoints for the same statistic_id. + """ + + meta: StatisticMetaData + stat: StatisticData + + +class StatisticDataBase(TypedDict): + """Mandatory fields for statistic data class.""" + + start: datetime + + +class StatisticData(StatisticDataBase, total=False): + """Statistic data class.""" + + mean: float + min: float + max: float + last_reset: datetime | None + state: float + sum: float + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr # type: ignore[misc] + def metadata_id(self) -> Column: + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: + """Create object from a statistics.""" + return cls( # type: ignore[call-arg,misc] + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start", + "metadata_id", + "start", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticMetaData(TypedDict): + """Statistic meta data class.""" + + has_mean: bool + has_sum: bool + name: str | None + source: str + statistic_id: str + unit_of_measurement: str | None + + +class StatisticsMeta(Base): # type: ignore[misc,valid-type] + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + name = Column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): # type: ignore[misc,valid-type] + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore[misc,valid-type] + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +class StatisticsRuns(Base): # type: ignore[misc,valid-type] + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True)) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +@overload +def process_timestamp(ts: None) -> None: + ... + + +@overload +def process_timestamp(ts: datetime) -> datetime: + ... + + +def process_timestamp(ts: datetime | None) -> datetime | None: + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) + + +@overload +def process_timestamp_to_utc_isoformat(ts: None) -> None: + ... + + +@overload +def process_timestamp_to_utc_isoformat(ts: datetime) -> str: + ... + + +def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: + """Process a timestamp into UTC isotime.""" + if ts is None: + return None + if ts.tzinfo == dt_util.UTC: + return ts.isoformat() + if ts.tzinfo is None: + return f"{ts.isoformat()}{DB_TIMEZONE}" + return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + "_attr_cache", + ] + + def __init__( # pylint: disable=super-init-not-called + self, row: Row, attr_cache: dict[str, dict[str, Any]] | None = None + ) -> None: + """Init the lazy state.""" + self._row = row + self.entity_id: str = self._row.entity_id + self.state = self._row.state or "" + self._attributes: dict[str, Any] | None = None + self._last_changed: datetime | None = None + self._last_updated: datetime | None = None + self._context: Context | None = None + self._attr_cache = attr_cache + + @property # type: ignore[override] + def attributes(self) -> dict[str, Any]: # type: ignore[override] + """State attributes.""" + if self._attributes is None: + source = self._row.shared_attrs or self._row.attributes + if self._attr_cache is not None and ( + attributes := self._attr_cache.get(source) + ): + self._attributes = attributes + return attributes + if source == EMPTY_JSON_OBJECT or source is None: + self._attributes = {} + return self._attributes + try: + self._attributes = json.loads(source) + except ValueError: + # When json.loads fails + _LOGGER.exception( + "Error converting row to state attributes: %s", self._row + ) + self._attributes = {} + if self._attr_cache is not None: + self._attr_cache[source] = self._attributes + return self._attributes + + @attributes.setter + def attributes(self, value: dict[str, Any]) -> None: + """Set attributes.""" + self._attributes = value + + @property # type: ignore[override] + def context(self) -> Context: # type: ignore[override] + """State context.""" + if self._context is None: + self._context = Context(id=None) # type: ignore[arg-type] + return self._context + + @context.setter + def context(self, value: Context) -> None: + """Set context.""" + self._context = value + + @property # type: ignore[override] + def last_changed(self) -> datetime: # type: ignore[override] + """Last changed datetime.""" + if self._last_changed is None: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value: datetime) -> None: + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore[override] + def last_updated(self) -> datetime: # type: ignore[override] + """Last updated datetime.""" + if self._last_updated is None: + if (last_updated := self._row.last_updated) is not None: + self._last_updated = process_timestamp(last_updated) + else: + self._last_updated = self.last_changed + return self._last_updated + + @last_updated.setter + def last_updated(self, value: datetime) -> None: + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self) -> dict[str, Any]: # type: ignore[override] + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed is None and self._last_updated is None: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if ( + self._row.last_updated is None + or self._row.last_changed == self._row.last_updated + ): + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + else: + last_changed_isoformat = self.last_changed.isoformat() + if self.last_changed == self.last_updated: + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = self.last_updated.isoformat() + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other: Any) -> bool: + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/recorder/test_backup.py b/tests/components/recorder/test_backup.py index d7c5a55b56a..e829c2aa13b 100644 --- a/tests/components/recorder/test_backup.py +++ b/tests/components/recorder/test_backup.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError async def test_async_pre_backup(hass: HomeAssistant, recorder_mock) -> None: """Test pre backup.""" with patch( - "homeassistant.components.recorder.backup.Recorder.lock_database" + "homeassistant.components.recorder.core.Recorder.lock_database" ) as lock_mock: await async_pre_backup(hass) assert lock_mock.called @@ -24,7 +24,7 @@ async def test_async_pre_backup_with_timeout( ) -> None: """Test pre backup with timeout.""" with patch( - "homeassistant.components.recorder.backup.Recorder.lock_database", + "homeassistant.components.recorder.core.Recorder.lock_database", side_effect=TimeoutError(), ) as lock_mock, pytest.raises(TimeoutError): await async_pre_backup(hass) @@ -45,7 +45,7 @@ async def test_async_pre_backup_with_migration( async def test_async_post_backup(hass: HomeAssistant, recorder_mock) -> None: """Test post backup.""" with patch( - "homeassistant.components.recorder.backup.Recorder.unlock_database" + "homeassistant.components.recorder.core.Recorder.unlock_database" ) as unlock_mock: await async_post_backup(hass) assert unlock_mock.called @@ -54,7 +54,7 @@ async def test_async_post_backup(hass: HomeAssistant, recorder_mock) -> None: async def test_async_post_backup_failure(hass: HomeAssistant, recorder_mock) -> None: """Test post backup failure.""" with patch( - "homeassistant.components.recorder.backup.Recorder.unlock_database", + "homeassistant.components.recorder.core.Recorder.unlock_database", return_value=False, ) as unlock_mock, pytest.raises(HomeAssistantError): await async_post_backup(hass) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 3e25a54e39d..0c3a41ab8ef 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -24,7 +24,7 @@ from homeassistant.components.recorder import ( Recorder, get_instance, ) -from homeassistant.components.recorder.const import DATA_INSTANCE, KEEPALIVE_TIME +from homeassistant.components.recorder.const import KEEPALIVE_TIME from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, EventData, @@ -51,6 +51,7 @@ from homeassistant.const import ( STATE_UNLOCKED, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util @@ -100,13 +101,14 @@ async def test_shutdown_before_startup_finishes( } hass.state = CoreState.not_running - await async_setup_recorder_instance(hass, config) - await hass.data[DATA_INSTANCE].async_db_ready - await hass.async_block_till_done() + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass, config)) + await recorder_helper.async_wait_recorder(hass) + instance = get_instance(hass) - session = await hass.async_add_executor_job(hass.data[DATA_INSTANCE].get_session) + session = await hass.async_add_executor_job(instance.get_session) - with patch.object(hass.data[DATA_INSTANCE], "engine"): + with patch.object(instance, "engine"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_stop() @@ -125,9 +127,11 @@ async def test_canceled_before_startup_finishes( ): """Test recorder shuts down when its startup future is canceled out from under it.""" hass.state = CoreState.not_running - await async_setup_recorder_instance(hass) + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass)) + await recorder_helper.async_wait_recorder(hass) + instance = get_instance(hass) - await instance.async_db_ready instance._hass_started.cancel() with patch.object(instance, "engine"): await hass.async_block_till_done() @@ -170,7 +174,9 @@ async def test_state_gets_saved_when_set_before_start_event( hass.state = CoreState.not_running - await async_setup_recorder_instance(hass) + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass)) + await recorder_helper.async_wait_recorder(hass) entity_id = "test.recorder" state = "restoring_from_db" @@ -214,14 +220,16 @@ async def test_saving_many_states( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test we expire after many commits.""" - await async_setup_recorder_instance(hass, {recorder.CONF_COMMIT_INTERVAL: 0}) + instance = await async_setup_recorder_instance( + hass, {recorder.CONF_COMMIT_INTERVAL: 0} + ) entity_id = "test.recorder" attributes = {"test_attr": 5, "test_attr_10": "nice"} - with patch.object( - hass.data[DATA_INSTANCE].event_session, "expire_all" - ) as expire_all, patch.object(recorder.core, "EXPIRE_AFTER_COMMITS", 2): + with patch.object(instance.event_session, "expire_all") as expire_all, patch.object( + recorder.core, "EXPIRE_AFTER_COMMITS", 2 + ): for _ in range(3): hass.states.async_set(entity_id, "on", attributes) await async_wait_recording_done(hass) @@ -269,14 +277,14 @@ def test_saving_state_with_exception(hass, hass_recorder, caplog): attributes = {"test_attr": 5, "test_attr_10": "nice"} def _throw_if_state_in_session(*args, **kwargs): - for obj in hass.data[DATA_INSTANCE].event_session: + for obj in get_instance(hass).event_session: if isinstance(obj, States): raise OperationalError( "insert the state", "fake params", "forced to fail" ) with patch("time.sleep"), patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "flush", side_effect=_throw_if_state_in_session, ): @@ -307,14 +315,14 @@ def test_saving_state_with_sqlalchemy_exception(hass, hass_recorder, caplog): attributes = {"test_attr": 5, "test_attr_10": "nice"} def _throw_if_state_in_session(*args, **kwargs): - for obj in hass.data[DATA_INSTANCE].event_session: + for obj in get_instance(hass).event_session: if isinstance(obj, States): raise SQLAlchemyError( "insert the state", "fake params", "forced to fail" ) with patch("time.sleep"), patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "flush", side_effect=_throw_if_state_in_session, ): @@ -390,7 +398,7 @@ def test_saving_event(hass, hass_recorder): assert len(events) == 1 event: Event = events[0] - hass.data[DATA_INSTANCE].block_till_done() + get_instance(hass).block_till_done() events: list[Event] = [] with session_scope(hass=hass) as session: @@ -421,7 +429,7 @@ def test_saving_event(hass, hass_recorder): def test_saving_state_with_commit_interval_zero(hass_recorder): """Test saving a state with a commit interval of zero.""" hass = hass_recorder({"commit_interval": 0}) - assert hass.data[DATA_INSTANCE].commit_interval == 0 + get_instance(hass).commit_interval == 0 entity_id = "test.recorder" state = "restoring_from_db" @@ -641,6 +649,7 @@ def test_saving_state_and_removing_entity(hass, hass_recorder): def test_recorder_setup_failure(hass): """Test some exceptions.""" + recorder_helper.async_initialize_recorder(hass) with patch.object(Recorder, "_setup_connection") as setup, patch( "homeassistant.components.recorder.core.time.sleep" ): @@ -655,6 +664,7 @@ def test_recorder_setup_failure(hass): def test_recorder_setup_failure_without_event_listener(hass): """Test recorder setup failure when the event listener is not setup.""" + recorder_helper.async_initialize_recorder(hass) with patch.object(Recorder, "_setup_connection") as setup, patch( "homeassistant.components.recorder.core.time.sleep" ): @@ -690,7 +700,7 @@ def run_tasks_at_time(hass, test_time): """Advance the clock and wait for any callbacks to finish.""" fire_time_changed(hass, test_time) hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() + get_instance(hass).block_till_done() @pytest.mark.parametrize("enable_nightly_purge", [True]) @@ -983,6 +993,7 @@ def test_compile_missing_statistics(tmpdir): ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1004,6 +1015,7 @@ def test_compile_missing_statistics(tmpdir): ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1195,6 +1207,7 @@ def test_service_disable_run_information_recorded(tmpdir): dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1216,6 +1229,7 @@ def test_service_disable_run_information_recorded(tmpdir): hass.stop() hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1244,6 +1258,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): test_db_file = await hass.async_add_executor_job(_create_tmpdir_for_test_db) dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl, CONF_COMMIT_INTERVAL: 0}} ) @@ -1258,7 +1273,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): sqlite3_exception.__cause__ = sqlite3.DatabaseError() with patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "close", side_effect=OperationalError("statement", {}, []), ): @@ -1267,7 +1282,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): await async_wait_recording_done(hass) with patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "commit", side_effect=[sqlite3_exception, None], ): @@ -1357,7 +1372,7 @@ async def test_database_lock_and_unlock( with session_scope(hass=hass) as session: return list(session.query(Events).filter_by(event_type=event_type)) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) assert await instance.lock_database() @@ -1399,7 +1414,7 @@ async def test_database_lock_and_overflow( with session_scope(hass=hass) as session: return list(session.query(Events).filter_by(event_type=event_type)) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) with patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1), patch.object( recorder.core, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.1 @@ -1424,7 +1439,7 @@ async def test_database_lock_timeout(hass, recorder_mock): """Test locking database timeout when recorder stopped.""" hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) class BlockQueue(recorder.tasks.RecorderTask): event: threading.Event = threading.Event() @@ -1447,7 +1462,7 @@ async def test_database_lock_without_instance(hass, recorder_mock): """Test database lock doesn't fail if instance is not initialized.""" hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) with patch.object(instance, "engine", None): try: assert await instance.lock_database() diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 38d6a191809..bbac01bb5d3 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -21,13 +21,13 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import db_schema, migration -from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, RecorderRuns, States, ) from homeassistant.components.recorder.util import session_scope +from homeassistant.helpers import recorder as recorder_helper import homeassistant.util.dt as dt_util from .common import async_wait_recording_done, create_engine_test @@ -54,6 +54,7 @@ async def test_schema_update_calls(hass): "homeassistant.components.recorder.migration._apply_update", wraps=migration._apply_update, ) as update: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -75,14 +76,15 @@ async def test_migration_in_progress(hass): """Test that we can check for migration in progress.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True,), patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ): + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) - await hass.data[DATA_INSTANCE].async_migration_event.wait() + await recorder.get_instance(hass).async_migration_event.wait() assert recorder.util.async_migration_in_progress(hass) is True await async_wait_recording_done(hass) @@ -106,13 +108,14 @@ async def test_database_migration_failed(hass): "homeassistant.components.persistent_notification.dismiss", side_effect=pn.dismiss, ) as mock_dismiss: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_add_executor_job(recorder.get_instance(hass).join) await hass.async_block_till_done() assert recorder.util.async_migration_in_progress(hass) is False @@ -137,6 +140,7 @@ async def test_database_migration_encounters_corruption(hass): ), patch( "homeassistant.components.recorder.core.move_away_broken_database" ) as move_away: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -166,13 +170,14 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): "homeassistant.components.persistent_notification.dismiss", side_effect=pn.dismiss, ) as mock_dismiss: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_add_executor_job(recorder.get_instance(hass).join) await hass.async_block_till_done() assert recorder.util.async_migration_in_progress(hass) is False @@ -190,6 +195,7 @@ async def test_events_during_migration_are_queued(hass): "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ): + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", @@ -201,7 +207,7 @@ async def test_events_during_migration_are_queued(hass): async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) - await hass.data[DATA_INSTANCE].async_recorder_ready.wait() + await recorder.get_instance(hass).async_recorder_ready.wait() await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False @@ -220,6 +226,7 @@ async def test_events_during_migration_queue_exhausted(hass): "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ), patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1): + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", @@ -232,7 +239,7 @@ async def test_events_during_migration_queue_exhausted(hass): async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) await hass.async_block_till_done() hass.states.async_set("my.entity", "off", {}) - await hass.data[DATA_INSTANCE].async_recorder_ready.wait() + await recorder.get_instance(hass).async_recorder_ready.wait() await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False @@ -248,8 +255,11 @@ async def test_events_during_migration_queue_exhausted(hass): assert len(db_states) == 2 -@pytest.mark.parametrize("start_version", [0, 16, 18, 22]) -async def test_schema_migrate(hass, start_version): +@pytest.mark.parametrize( + "start_version,live", + [(0, True), (16, True), (18, True), (22, True), (25, True)], +) +async def test_schema_migrate(hass, start_version, live): """Test the full schema migration logic. We're just testing that the logic can execute successfully here without @@ -260,7 +270,8 @@ async def test_schema_migrate(hass, start_version): migration_done = threading.Event() migration_stall = threading.Event() migration_version = None - real_migration = recorder.migration.migrate_schema + real_migrate_schema = recorder.migration.migrate_schema + real_apply_update = recorder.migration._apply_update def _create_engine_test(*args, **kwargs): """Test version of create_engine that initializes with old schema. @@ -285,14 +296,12 @@ async def test_schema_migrate(hass, start_version): start=self.run_history.recording_start, created=dt_util.utcnow() ) - def _instrument_migration(*args): + def _instrument_migrate_schema(*args): """Control migration progress and check results.""" nonlocal migration_done nonlocal migration_version - nonlocal migration_stall - migration_stall.wait() try: - real_migration(*args) + real_migrate_schema(*args) except Exception: migration_done.set() raise @@ -308,6 +317,12 @@ async def test_schema_migrate(hass, start_version): migration_version = res.schema_version migration_done.set() + def _instrument_apply_update(*args): + """Control migration progress.""" + nonlocal migration_stall + migration_stall.wait() + real_apply_update(*args) + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=_create_engine_test, @@ -317,12 +332,21 @@ async def test_schema_migrate(hass, start_version): autospec=True, ) as setup_run, patch( "homeassistant.components.recorder.migration.migrate_schema", - wraps=_instrument_migration, + wraps=_instrument_migrate_schema, + ), patch( + "homeassistant.components.recorder.migration._apply_update", + wraps=_instrument_apply_update, ): - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + recorder_helper.async_initialize_recorder(hass) + hass.async_create_task( + async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) ) + await recorder_helper.async_wait_recorder(hass) + assert recorder.util.async_migration_in_progress(hass) is True + assert recorder.util.async_migration_is_live(hass) == live migration_stall.set() await hass.async_block_till_done() await hass.async_add_executor_job(migration_done.wait) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 48639790d0d..8db4587f1cf 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -12,11 +12,12 @@ from sqlalchemy.orm import Session from homeassistant.components import recorder from homeassistant.components.recorder import history, statistics -from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX +from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import StatisticsShortTerm from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( async_add_external_statistics, + async_import_statistics, delete_statistics_duplicates, delete_statistics_meta_duplicates, get_last_short_term_statistics, @@ -30,6 +31,7 @@ from homeassistant.components.recorder.util import session_scope from homeassistant.const import TEMP_CELSIUS from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -44,7 +46,7 @@ ORIG_TZ = dt_util.DEFAULT_TIME_ZONE def test_compile_hourly_statistics(hass_recorder): """Test compiling hourly statistics.""" hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] + instance = recorder.get_instance(hass) setup_component(hass, "sensor", {}) zero, four, states = record_states(hass) hist = history.get_significant_states(hass, zero, four) @@ -141,7 +143,7 @@ def test_compile_hourly_statistics(hass_recorder): stats = get_last_short_term_statistics(hass, 1, "sensor.test3", True) assert stats == {} - recorder.get_session().query(StatisticsShortTerm).delete() + instance.get_session().query(StatisticsShortTerm).delete() # Should not fail there is nothing in the table stats = get_latest_short_term_statistics(hass, ["sensor.test1"]) assert stats == {} @@ -437,26 +439,45 @@ def test_statistics_duplicated(hass_recorder, caplog): caplog.clear() -async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): - """Test inserting external statistics.""" +@pytest.mark.parametrize("last_reset_str", ("2022-01-01T00:00:00+02:00", None)) +@pytest.mark.parametrize( + "source, statistic_id, import_fn", + ( + ("test", "test:total_energy_import", async_add_external_statistics), + ("recorder", "sensor.total_energy_import", async_import_statistics), + ), +) +async def test_import_statistics( + hass, + hass_ws_client, + recorder_mock, + caplog, + source, + statistic_id, + import_fn, + last_reset_str, +): + """Test importing statistics and inserting external statistics.""" client = await hass_ws_client() assert "Compiling statistics for" not in caplog.text assert "Statistics already compiled" not in caplog.text zero = dt_util.utcnow() + last_reset = dt_util.parse_datetime(last_reset_str) if last_reset_str else None + last_reset_utc_str = dt_util.as_utc(last_reset).isoformat() if last_reset else None period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) external_statistics1 = { "start": period1, - "last_reset": None, + "last_reset": last_reset, "state": 0, "sum": 2, } external_statistics2 = { "start": period2, - "last_reset": None, + "last_reset": last_reset, "state": 1, "sum": 3, } @@ -465,37 +486,35 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "has_mean": False, "has_sum": True, "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import", + "source": source, + "statistic_id": statistic_id, "unit_of_measurement": "kWh", } - async_add_external_statistics( - hass, external_metadata, (external_statistics1, external_statistics2) - ) + import_fn(hass, external_metadata, (external_statistics1, external_statistics2)) await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(0.0), "sum": approx(2.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -506,37 +525,37 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): { "has_mean": False, "has_sum": True, - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "name": "Total imported energy", - "source": "test", + "source": source, "unit_of_measurement": "kWh", } ] - metadata = get_metadata(hass, statistic_ids=("test:total_energy_import",)) + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) assert metadata == { - "test:total_energy_import": ( + statistic_id: ( 1, { "has_mean": False, "has_sum": True, "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import", + "source": source, + "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, ) } - last_stats = get_last_statistics(hass, 1, "test:total_energy_import", True) + last_stats = get_last_statistics(hass, 1, statistic_id, True) assert last_stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -550,13 +569,13 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "state": 5, "sum": 6, } - async_add_external_statistics(hass, external_metadata, (external_statistics,)) + import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": None, @@ -567,13 +586,13 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "sum": approx(6.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -586,34 +605,34 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "max": 1, "mean": 2, "min": 3, - "last_reset": None, + "last_reset": last_reset, "state": 4, "sum": 5, } - async_add_external_statistics(hass, external_metadata, (external_statistics,)) + import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": approx(1.0), "mean": approx(2.0), "min": approx(3.0), - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(4.0), "sum": approx(5.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -624,7 +643,7 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): { "id": 1, "type": "recorder/adjust_sum_statistics", - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, } @@ -635,26 +654,26 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": approx(1.0), "mean": approx(2.0), "min": approx(3.0), - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(4.0), "sum": approx(5.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(1003.0), }, @@ -670,11 +689,12 @@ def test_external_statistics_errors(hass_recorder, caplog): assert "Statistics already compiled" not in caplog.text zero = dt_util.utcnow() + last_reset = zero.replace(minute=0, second=0, microsecond=0) - timedelta(days=1) period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) _external_statistics = { "start": period1, - "last_reset": None, + "last_reset": last_reset, "state": 0, "sum": 2, } @@ -711,7 +731,7 @@ def test_external_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} - # Attempt to insert statistics for an naive starting time + # Attempt to insert statistics for a naive starting time external_metadata = {**_external_metadata} external_statistics = { **_external_statistics, @@ -734,6 +754,106 @@ def test_external_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + # Attempt to insert statistics with a naive last_reset + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "last_reset": last_reset.replace(tzinfo=None), + } + with pytest.raises(HomeAssistantError): + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + + +def test_import_statistics_errors(hass_recorder, caplog): + """Test validation of imported statistics.""" + hass = hass_recorder() + wait_recording_done(hass) + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + last_reset = zero.replace(minute=0, second=0, microsecond=0) - timedelta(days=1) + period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + + _external_statistics = { + "start": period1, + "last_reset": last_reset, + "state": 0, + "sum": 2, + } + + _external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "recorder", + "statistic_id": "sensor.total_energy_import", + "unit_of_measurement": "kWh", + } + + # Attempt to insert statistics for an external source + external_metadata = { + **_external_metadata, + "statistic_id": "test:total_energy_import", + } + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + + # Attempt to insert statistics for the wrong domain + external_metadata = {**_external_metadata, "source": "sensor"} + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + + # Attempt to insert statistics for a naive starting time + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "start": period1.replace(tzinfo=None), + } + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + + # Attempt to insert statistics for an invalid starting time + external_metadata = {**_external_metadata} + external_statistics = {**_external_statistics, "start": period1.replace(minute=1)} + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + + # Attempt to insert statistics with a naive last_reset + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "last_reset": last_reset.replace(tzinfo=None), + } + with pytest.raises(HomeAssistantError): + async_import_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + @pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"]) @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") @@ -1009,6 +1129,7 @@ def test_delete_metadata_duplicates(caplog, tmpdir): "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -1039,6 +1160,7 @@ def test_delete_metadata_duplicates(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 28 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -1098,6 +1220,7 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -1130,6 +1253,7 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 28 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index 50311a987d6..a7cc2b35e61 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -16,6 +16,7 @@ from sqlalchemy.orm import Session from homeassistant.components import recorder from homeassistant.components.recorder import SQLITE_URL_PREFIX, statistics from homeassistant.components.recorder.util import session_scope +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -179,6 +180,7 @@ def test_delete_duplicates(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -206,6 +208,7 @@ def test_delete_duplicates(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -347,6 +350,7 @@ def test_delete_duplicates_many(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -380,6 +384,7 @@ def test_delete_duplicates_many(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -492,6 +497,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -515,6 +521,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() hass.config.config_dir = tmpdir + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -592,6 +599,7 @@ def test_delete_duplicates_short_term(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -614,6 +622,7 @@ def test_delete_duplicates_short_term(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() hass.config.config_dir = tmpdir + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 8624719f951..ac4eeada3d3 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -13,7 +13,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util -from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX +from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.models import UnsupportedDialect from homeassistant.components.recorder.util import ( @@ -35,7 +35,7 @@ def test_session_scope_not_setup(hass_recorder): """Try to create a session scope when not setup.""" hass = hass_recorder() with patch.object( - hass.data[DATA_INSTANCE], "get_session", return_value=None + util.get_instance(hass), "get_session", return_value=None ), pytest.raises(RuntimeError): with util.session_scope(hass=hass): pass @@ -547,7 +547,7 @@ def test_basic_sanity_check(hass_recorder): """Test the basic sanity checks with a missing table.""" hass = hass_recorder() - cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor() + cursor = util.get_instance(hass).engine.raw_connection().cursor() assert util.basic_sanity_check(cursor) is True @@ -560,7 +560,7 @@ def test_basic_sanity_check(hass_recorder): def test_combined_checks(hass_recorder, caplog): """Run Checks on the open database.""" hass = hass_recorder() - instance = recorder.get_instance(hass) + instance = util.get_instance(hass) instance.db_retry_wait = 0 cursor = instance.engine.raw_connection().cursor() @@ -639,8 +639,8 @@ def test_end_incomplete_runs(hass_recorder, caplog): def test_periodic_db_cleanups(hass_recorder): """Test periodic db cleanups.""" hass = hass_recorder() - with patch.object(hass.data[DATA_INSTANCE].engine, "connect") as connect_mock: - util.periodic_db_cleanups(hass.data[DATA_INSTANCE]) + with patch.object(util.get_instance(hass).engine, "connect") as connect_mock: + util.periodic_db_cleanups(util.get_instance(hass)) text_obj = connect_mock.return_value.__enter__.return_value.execute.mock_calls[0][ 1 @@ -663,11 +663,9 @@ async def test_write_lock_db( config = { recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db?timeout=0.1") } - await async_setup_recorder_instance(hass, config) + instance = await async_setup_recorder_instance(hass, config) await hass.async_block_till_done() - instance = hass.data[DATA_INSTANCE] - def _drop_table(): with instance.engine.connect() as connection: connection.execute(text("DROP TABLE events;")) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 55bbb13898e..b604cc53e6c 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -8,8 +8,14 @@ import pytest from pytest import approx from homeassistant.components import recorder -from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.statistics import async_add_external_statistics +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, + get_metadata, + list_statistic_ids, + statistics_during_period, +) +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM @@ -269,6 +275,7 @@ async def test_recorder_info(hass, hass_ws_client, recorder_mock): "backlog": 0, "max_backlog": 40000, "migration_in_progress": False, + "migration_is_live": False, "recording": True, "thread_running": True, } @@ -291,6 +298,7 @@ async def test_recorder_info_bad_recorder_config(hass, hass_ws_client): client = await hass_ws_client() with patch("homeassistant.components.recorder.migration.migrate_schema"): + recorder_helper.async_initialize_recorder(hass) assert not await async_setup_component( hass, recorder.DOMAIN, {recorder.DOMAIN: config} ) @@ -298,7 +306,7 @@ async def test_recorder_info_bad_recorder_config(hass, hass_ws_client): await hass.async_block_till_done() # Wait for recorder to shut down - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_add_executor_job(recorder.get_instance(hass).join) await client.send_json({"id": 1, "type": "recorder/info"}) response = await client.receive_json() @@ -313,7 +321,7 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): migration_done = threading.Event() - real_migration = recorder.migration.migrate_schema + real_migration = recorder.migration._apply_update def stalled_migration(*args): """Make migration stall.""" @@ -329,12 +337,16 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): ), patch.object( recorder.core, "MAX_QUEUE_BACKLOG", 1 ), patch( - "homeassistant.components.recorder.migration.migrate_schema", + "homeassistant.components.recorder.migration._apply_update", wraps=stalled_migration, ): - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + recorder_helper.async_initialize_recorder(hass) + hass.create_task( + async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) ) + await recorder_helper.async_wait_recorder(hass) hass.states.async_set("my.entity", "on", {}) await hass.async_block_till_done() @@ -547,3 +559,270 @@ async def test_get_statistics_metadata( "unit_of_measurement": unit, } ] + + +@pytest.mark.parametrize( + "source, statistic_id", + ( + ("test", "test:total_energy_import"), + ("recorder", "sensor.total_energy_import"), + ), +) +async def test_import_statistics( + hass, hass_ws_client, recorder_mock, caplog, source, statistic_id +): + """Test importing statistics.""" + client = await hass_ws_client() + + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) + + external_statistics1 = { + "start": period1.isoformat(), + "last_reset": None, + "state": 0, + "sum": 2, + } + external_statistics2 = { + "start": period2.isoformat(), + "last_reset": None, + "state": 1, + "sum": 3, + } + + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + } + + await client.send_json( + { + "id": 1, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics1, external_statistics2], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(0.0), + "sum": approx(2.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + statistic_ids = list_statistic_ids(hass) # TODO + assert statistic_ids == [ + { + "has_mean": False, + "has_sum": True, + "statistic_id": statistic_id, + "name": "Total imported energy", + "source": source, + "unit_of_measurement": "kWh", + } + ] + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) + assert metadata == { + statistic_id: ( + 1, + { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + }, + ) + } + last_stats = get_last_statistics(hass, 1, statistic_id, True) + assert last_stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + + # Update the previously inserted statistics + external_statistics = { + "start": period1.isoformat(), + "last_reset": None, + "state": 5, + "sum": 6, + } + + await client.send_json( + { + "id": 2, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(5.0), + "sum": approx(6.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + + # Update the previously inserted statistics + external_statistics = { + "start": period1.isoformat(), + "max": 1, + "mean": 2, + "min": 3, + "last_reset": None, + "state": 4, + "sum": 5, + } + + await client.send_json( + { + "id": 3, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), + "last_reset": None, + "state": approx(4.0), + "sum": approx(5.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + + await client.send_json( + { + "id": 4, + "type": "recorder/adjust_sum_statistics", + "statistic_id": statistic_id, + "start_time": period2.isoformat(), + "adjustment": 1000.0, + } + ) + response = await client.receive_json() + assert response["success"] + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), + "last_reset": None, + "state": approx(4.0), + "sum": approx(5.0), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(1003.0), + }, + ] + } diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index ec5eae468fc..a269106c159 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -38,7 +38,7 @@ async def test_config_flow_single_account( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} # Failed credentials @@ -55,7 +55,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_credentials"} renault_account = AsyncMock() @@ -82,7 +82,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_1" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -97,7 +97,7 @@ async def test_config_flow_no_account(hass: HomeAssistant, mock_setup_entry: Asy result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} # Account list empty @@ -114,7 +114,7 @@ async def test_config_flow_no_account(hass: HomeAssistant, mock_setup_entry: Asy }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "kamereon_no_account" assert len(mock_setup_entry.mock_calls) == 0 @@ -127,7 +127,7 @@ async def test_config_flow_multiple_accounts( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} renault_account_1 = RenaultAccount( @@ -153,7 +153,7 @@ async def test_config_flow_multiple_accounts( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "kamereon" # Account selected @@ -161,7 +161,7 @@ async def test_config_flow_multiple_accounts( result["flow_id"], user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_2" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -179,7 +179,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant, mock_setup_entry: Asyn result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} renault_account = RenaultAccount( @@ -199,7 +199,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant, mock_setup_entry: Asyn }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -220,7 +220,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result["errors"] == {} @@ -234,7 +234,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): user_input={CONF_PASSWORD: "any"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result2["errors"] == {"base": "invalid_credentials"} @@ -245,5 +245,5 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): user_input={CONF_PASSWORD: "any"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/repairs/__init__.py b/tests/components/repairs/__init__.py new file mode 100644 index 00000000000..77971d0284b --- /dev/null +++ b/tests/components/repairs/__init__.py @@ -0,0 +1,30 @@ +"""Tests for the repairs integration.""" +from collections.abc import Awaitable, Callable + +from aiohttp import ClientWebSocketResponse + +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def get_repairs( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +): + """Return the repairs list of issues.""" + assert await async_setup_component(hass, "repairs", {}) + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] + + return msg["result"]["issues"] diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py new file mode 100644 index 00000000000..d70f6c6e11d --- /dev/null +++ b/tests/components/repairs/test_init.py @@ -0,0 +1,532 @@ +"""Test the repairs websocket API.""" +from collections.abc import Awaitable, Callable +from unittest.mock import AsyncMock, Mock + +from aiohttp import ClientWebSocketResponse +from freezegun import freeze_time +import pytest + +from homeassistant.components.repairs import ( + async_create_issue, + async_delete_issue, + create_issue, + delete_issue, +) +from homeassistant.components.repairs.const import DOMAIN +from homeassistant.components.repairs.issue_handler import ( + async_ignore_issue, + async_process_repairs_platforms, +) +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import mock_platform + + +@freeze_time("2022-07-19 07:53:05") +async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test creating and updating issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9.0dev0", + "domain": "test", + "issue_id": "issue_1", + "is_fixable": True, + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "is_fixable": False, + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + for issue in issues + ] + } + + # Update an issue + async_create_issue( + hass, + issues[0]["domain"], + issues[0]["issue_id"], + breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + is_fixable=issues[0]["is_fixable"], + issue_domain="my_issue_domain", + learn_more_url="blablabla", + severity=issues[0]["severity"], + translation_key=issues[0]["translation_key"], + translation_placeholders=issues[0]["translation_placeholders"], + ) + + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["issues"][0] == dict( + issues[0], + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + learn_more_url="blablabla", + issue_domain="my_issue_domain", + ) + + +@pytest.mark.parametrize("ha_version", ("2022.9.cat", "In the future: 2023.1.1")) +async def test_create_issue_invalid_version( + hass: HomeAssistant, hass_ws_client, ha_version +) -> None: + """Test creating an issue with invalid breaks in version.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issue = { + "breaks_in_ha_version": ha_version, + "domain": "test", + "issue_id": "issue_1", + "is_fixable": True, + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + } + + with pytest.raises(Exception): + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + +@freeze_time("2022-07-19 07:53:05") +async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test ignoring issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "is_fixable": True, + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + for issue in issues + ] + } + + # Ignore a non-existing issue + with pytest.raises(KeyError): + async_ignore_issue(hass, issues[0]["domain"], "no_such_issue", True) + + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + for issue in issues + ] + } + + # Ignore an existing issue + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) + + await client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=ha_version, + ignored=True, + issue_domain=None, + ) + for issue in issues + ] + } + + # Ignore the same issue again + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) + + await client.send_json({"id": 5, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=ha_version, + ignored=True, + issue_domain=None, + ) + for issue in issues + ] + } + + # Update an ignored issue + async_create_issue( + hass, + issues[0]["domain"], + issues[0]["issue_id"], + breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + is_fixable=issues[0]["is_fixable"], + learn_more_url="blablabla", + severity=issues[0]["severity"], + translation_key=issues[0]["translation_key"], + translation_placeholders=issues[0]["translation_placeholders"], + ) + + await client.send_json({"id": 6, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["issues"][0] == dict( + issues[0], + created="2022-07-19T07:53:05+00:00", + dismissed_version=ha_version, + ignored=True, + learn_more_url="blablabla", + issue_domain=None, + ) + + # Unignore the same issue + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], False) + + await client.send_json({"id": 7, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + learn_more_url="blablabla", + issue_domain=None, + ) + for issue in issues + ] + } + + +async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> None: + """Test we can delete an issue.""" + freezer.move_to("2022-07-19 07:53:05") + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "fake_integration", + "issue_id": "issue_1", + "is_fixable": True, + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + for issue in issues + ] + } + + # Delete a non-existing issue + async_delete_issue(hass, issues[0]["domain"], "no_such_issue") + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + for issue in issues + ] + } + + # Delete an existing issue + async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + # Delete the same issue again + async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + # Create the same issues again created timestamp should change + freezer.move_to("2022-07-19 08:53:05") + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 5, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T08:53:05+00:00", + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + for issue in issues + ] + } + + +async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> None: + """Test non-compliant platforms are not registered.""" + + hass.config.components.add("fake_integration") + hass.config.components.add("integration_without_diagnostics") + mock_platform( + hass, + "fake_integration.repairs", + Mock(async_create_fix_flow=AsyncMock(return_value=True)), + ) + mock_platform( + hass, + "integration_without_diagnostics.repairs", + Mock(spec=[]), + ) + assert await async_setup_component(hass, DOMAIN, {}) + + await async_process_repairs_platforms(hass) + + assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"] + + +@freeze_time("2022-07-21 08:22:00") +async def test_sync_methods( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test sync method for creating and deleting an issue.""" + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + def _create_issue() -> None: + create_issue( + hass, + "fake_integration", + "sync_issue", + breaks_in_ha_version="2022.9", + is_fixable=True, + learn_more_url="https://theuselessweb.com", + severity=IssueSeverity.ERROR, + translation_key="abc_123", + translation_placeholders={"abc": "123"}, + ) + + await hass.async_add_executor_job(_create_issue) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2022.9", + "created": "2022-07-21T08:22:00+00:00", + "dismissed_version": None, + "domain": "fake_integration", + "ignored": False, + "is_fixable": True, + "issue_id": "sync_issue", + "issue_domain": None, + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + } + ] + } + + await hass.async_add_executor_job( + delete_issue, hass, "fake_integration", "sync_issue" + ) + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} diff --git a/tests/components/repairs/test_issue_registry.py b/tests/components/repairs/test_issue_registry.py new file mode 100644 index 00000000000..523f75bfdc2 --- /dev/null +++ b/tests/components/repairs/test_issue_registry.py @@ -0,0 +1,151 @@ +"""Test the repairs websocket API.""" +from homeassistant.components.repairs import async_create_issue, issue_registry +from homeassistant.components.repairs.const import DOMAIN +from homeassistant.components.repairs.issue_handler import ( + async_delete_issue, + async_ignore_issue, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import async_capture_events, flush_store + + +async def test_load_issues(hass: HomeAssistant) -> None: + """Make sure that we can load/save data correctly.""" + assert await async_setup_component(hass, DOMAIN, {}) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "is_fixable": True, + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "is_fixable": True, + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + { + "breaks_in_ha_version": "2022.7", + "domain": "test", + "issue_id": "issue_3", + "is_fixable": True, + "learn_more_url": "https://checkboxrace.com", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "789"}, + }, + ] + + events = async_capture_events( + hass, issue_registry.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED + ) + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await hass.async_block_till_done() + + assert len(events) == 3 + assert events[0].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_1", + } + assert events[1].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_2", + } + assert events[2].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_3", + } + + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) + await hass.async_block_till_done() + + assert len(events) == 4 + assert events[3].data == { + "action": "update", + "domain": "test", + "issue_id": "issue_1", + } + + async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) + await hass.async_block_till_done() + + assert len(events) == 5 + assert events[4].data == { + "action": "remove", + "domain": "test", + "issue_id": "issue_3", + } + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 2 + issue1 = registry.async_get_issue("test", "issue_1") + issue2 = registry.async_get_issue("test", "issue_2") + + registry2 = issue_registry.IssueRegistry(hass) + await flush_store(registry._store) + await registry2.async_load() + + assert list(registry.issues) == list(registry2.issues) + + issue1_registry2 = registry2.async_get_issue("test", "issue_1") + assert issue1_registry2.created == issue1.created + assert issue1_registry2.dismissed_version == issue1.dismissed_version + issue2_registry2 = registry2.async_get_issue("test", "issue_2") + assert issue2_registry2.created == issue2.created + assert issue2_registry2.dismissed_version == issue2.dismissed_version + + +async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> None: + """Test loading stored issues on start.""" + hass_storage[issue_registry.STORAGE_KEY] = { + "version": issue_registry.STORAGE_VERSION, + "data": { + "issues": [ + { + "created": "2022-07-19T09:41:13.746514+00:00", + "dismissed_version": "2022.7.0.dev0", + "domain": "test", + "issue_id": "issue_1", + }, + { + "created": "2022-07-19T19:41:13.746514+00:00", + "dismissed_version": None, + "domain": "test", + "issue_id": "issue_2", + }, + ] + }, + } + + assert await async_setup_component(hass, DOMAIN, {}) + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 2 diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py new file mode 100644 index 00000000000..d778b043832 --- /dev/null +++ b/tests/components/repairs/test_websocket_api.py @@ -0,0 +1,458 @@ +"""Test the repairs websocket API.""" +from __future__ import annotations + +from http import HTTPStatus +from unittest.mock import ANY, AsyncMock, Mock + +from freezegun import freeze_time +import pytest +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import ( + RepairsFlow, + async_create_issue, + issue_registry, +) +from homeassistant.components.repairs.const import DOMAIN +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import mock_platform + + +async def create_issues(hass, ws_client): + """Create issues.""" + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "fake_integration", + "issue_id": "issue_1", + "is_fixable": True, + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created=ANY, + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + for issue in issues + ] + } + + return issues + + +class MockFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await (self.async_step_confirm()) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + return self.async_create_entry(title=None, data=None) + + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +@pytest.fixture(autouse=True) +async def mock_repairs_integration(hass): + """Mock a repairs integration.""" + hass.config.components.add("fake_integration") + hass.config.components.add("integration_without_diagnostics") + + def async_create_fix_flow(hass, issue_id): + return MockFixFlow() + + mock_platform( + hass, + "fake_integration.repairs", + Mock(async_create_fix_flow=AsyncMock(wraps=async_create_fix_flow)), + ) + mock_platform( + hass, + "integration_without_diagnostics.repairs", + Mock(spec=[]), + ) + + +async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can dismiss an issue.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issues = await create_issues(hass, client) + + await client.send_json( + { + "id": 2, + "type": "repairs/ignore_issue", + "domain": "fake_integration", + "issue_id": "no_such_issue", + "ignore": True, + } + ) + msg = await client.receive_json() + assert not msg["success"] + + await client.send_json( + { + "id": 3, + "type": "repairs/ignore_issue", + "domain": "fake_integration", + "issue_id": "issue_1", + "ignore": True, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + await client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created=ANY, + dismissed_version=ha_version, + ignored=True, + issue_domain=None, + ) + for issue in issues + ] + } + + await client.send_json( + { + "id": 5, + "type": "repairs/ignore_issue", + "domain": "fake_integration", + "issue_id": "issue_1", + "ignore": False, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + await client.send_json({"id": 6, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created=ANY, + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + for issue in issues + ] + } + + +async def test_fix_non_existing_issue( + hass: HomeAssistant, hass_client, hass_ws_client +) -> None: + """Test trying to fix an issue that doesn't exist.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + issues = await create_issues(hass, ws_client) + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": "no_such_integration", "issue_id": "no_such_issue"} + ) + + assert resp.status != HTTPStatus.OK + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "no_such_issue"} + ) + + assert resp.status != HTTPStatus.OK + + await ws_client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created=ANY, + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + for issue in issues + ] + } + + +async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> None: + """Test we can fix an issue.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues(hass, ws_client) + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "data_schema": [], + "description_placeholders": None, + "errors": None, + "flow_id": ANY, + "handler": "fake_integration", + "last_step": None, + "step_id": "confirm", + "type": "form", + } + + url = f"/api/repairs/issues/fix/{flow_id}" + # Test we can get the status of the flow + resp2 = await client.get(url) + + assert resp2.status == HTTPStatus.OK + data2 = await resp2.json() + + assert data == data2 + + resp = await client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "description": None, + "description_placeholders": None, + "flow_id": flow_id, + "handler": "fake_integration", + "title": None, + "type": "create_entry", + "version": 1, + } + + await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_fix_issue_unauth( + hass: HomeAssistant, hass_client, hass_admin_user +) -> None: + """Test we can't query the result if not authorized.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + hass_admin_user.groups = [] + + client = await hass_client() + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + + assert resp.status == HTTPStatus.UNAUTHORIZED + + +async def test_get_progress_unauth( + hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user +) -> None: + """Test we can't fix an issue if not authorized.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues(hass, ws_client) + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + flow_id = data["flow_id"] + + hass_admin_user.groups = [] + + url = f"/api/repairs/issues/fix/{flow_id}" + # Test we can't get the status of the flow + resp = await client.get(url) + assert resp.status == HTTPStatus.UNAUTHORIZED + + +async def test_step_unauth( + hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user +) -> None: + """Test we can't fix an issue if not authorized.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues(hass, ws_client) + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + flow_id = data["flow_id"] + + hass_admin_user.groups = [] + + url = f"/api/repairs/issues/fix/{flow_id}" + # Test we can't get the status of the flow + resp = await client.post(url) + assert resp.status == HTTPStatus.UNAUTHORIZED + + +@freeze_time("2022-07-19 07:53:05") +async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> None: + """Test we can list issues.""" + + # Add an inactive issue, this should not be exposed in the list + hass_storage[issue_registry.STORAGE_KEY] = { + "version": issue_registry.STORAGE_VERSION, + "data": { + "issues": [ + { + "created": "2022-07-19T09:41:13.746514+00:00", + "dismissed_version": None, + "domain": "test", + "issue_id": "issue_3_inactive", + "issue_domain": None, + }, + ] + }, + } + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "is_fixable": True, + "issue_id": "issue_1", + "issue_domain": None, + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "is_fixable": False, + "issue_id": "issue_2", + "issue_domain": None, + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + ) + for issue in issues + ] + } diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index eaa7f1c79a1..7679dbbf37e 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -314,7 +314,7 @@ async def test_options_global(hass): user_input={"automatic_add": True, "protocols": SOME_PROTOCOLS}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -349,7 +349,7 @@ async def test_no_protocols(hass): user_input={"automatic_add": False, "protocols": []}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -404,7 +404,7 @@ async def test_options_add_device(hass): result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -475,11 +475,11 @@ async def test_options_replace_sensor_device(hass): await start_options_flow(hass, entry) state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_signal_strength" ) assert state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery" ) assert state state = hass.states.get( @@ -495,11 +495,11 @@ async def test_options_replace_sensor_device(hass): ) assert state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_signal_strength" ) assert state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery" ) assert state state = hass.states.get( @@ -558,14 +558,14 @@ async def test_options_replace_sensor_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() entity_registry = er.async_get(hass) entry = entity_registry.async_get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_signal_strength" ) assert entry assert entry.device_id == new_device @@ -580,7 +580,7 @@ async def test_options_replace_sensor_device(hass): assert entry assert entry.device_id == new_device entry = entity_registry.async_get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery" ) assert entry assert entry.device_id == new_device @@ -591,11 +591,11 @@ async def test_options_replace_sensor_device(hass): assert entry.device_id == new_device state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_signal_strength" ) assert not state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery" ) assert not state state = hass.states.get( @@ -637,13 +637,13 @@ async def test_options_replace_control_device(hass): state = hass.states.get("binary_sensor.ac_118cdea_2") assert state - state = hass.states.get("sensor.ac_118cdea_2_rssi_numeric") + state = hass.states.get("sensor.ac_118cdea_2_signal_strength") assert state state = hass.states.get("switch.ac_118cdea_2") assert state state = hass.states.get("binary_sensor.ac_1118cdea_2") assert state - state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric") + state = hass.states.get("sensor.ac_1118cdea_2_signal_strength") assert state state = hass.states.get("switch.ac_1118cdea_2") assert state @@ -691,7 +691,7 @@ async def test_options_replace_control_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -700,7 +700,7 @@ async def test_options_replace_control_device(hass): entry = entity_registry.async_get("binary_sensor.ac_118cdea_2") assert entry assert entry.device_id == new_device - entry = entity_registry.async_get("sensor.ac_118cdea_2_rssi_numeric") + entry = entity_registry.async_get("sensor.ac_118cdea_2_signal_strength") assert entry assert entry.device_id == new_device entry = entity_registry.async_get("switch.ac_118cdea_2") @@ -709,7 +709,7 @@ async def test_options_replace_control_device(hass): state = hass.states.get("binary_sensor.ac_1118cdea_2") assert not state - state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric") + state = hass.states.get("sensor.ac_1118cdea_2_signal_strength") assert not state state = hass.states.get("switch.ac_1118cdea_2") assert not state @@ -772,7 +772,7 @@ async def test_options_add_and_configure_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -816,7 +816,7 @@ async def test_options_add_and_configure_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -899,7 +899,7 @@ async def test_options_configure_rfy_cover_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 77f9960de49..8ba0104cf50 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -101,19 +101,19 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): assert state.attributes.get("friendly_name") == f"{base_name} Humidity status" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == f"{base_name} Rssi numeric" + assert state.attributes.get("friendly_name") == f"{base_name} Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT ) - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == f"{base_name} Battery numeric" + assert state.attributes.get("friendly_name") == f"{base_name} Battery" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -179,7 +179,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "normal" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "-64" assert ( @@ -192,7 +192,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "18.4" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -211,7 +211,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "normal" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "-64" assert ( @@ -224,7 +224,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "14.9" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -243,7 +243,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "normal" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "-64" assert ( @@ -256,7 +256,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "17.9" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -328,19 +328,19 @@ async def test_rssi_sensor(hass, rfxtrx): await hass.async_block_till_done() await hass.async_start() - state = hass.states.get("sensor.pt2262_22670e_rssi_numeric") + state = hass.states.get("sensor.pt2262_22670e_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == "PT2262 22670e Rssi numeric" + assert state.attributes.get("friendly_name") == "PT2262 22670e Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT ) - state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") + state = hass.states.get("sensor.ac_213c7f2_48_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == "AC 213c7f2:48 Rssi numeric" + assert state.attributes.get("friendly_name") == "AC 213c7f2:48 Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT @@ -349,21 +349,21 @@ async def test_rssi_sensor(hass, rfxtrx): await rfxtrx.signal("0913000022670e013b70") await rfxtrx.signal("0b1100cd0213c7f230010f71") - state = hass.states.get("sensor.pt2262_22670e_rssi_numeric") + state = hass.states.get("sensor.pt2262_22670e_signal_strength") assert state assert state.state == "-64" - state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") + state = hass.states.get("sensor.ac_213c7f2_48_signal_strength") assert state assert state.state == "-64" await rfxtrx.signal("0913000022670e013b60") await rfxtrx.signal("0b1100cd0213c7f230010f61") - state = hass.states.get("sensor.pt2262_22670e_rssi_numeric") + state = hass.states.get("sensor.pt2262_22670e_signal_strength") assert state assert state.state == "-72" - state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") + state = hass.states.get("sensor.ac_213c7f2_48_signal_strength") assert state assert state.state == "-72" diff --git a/tests/components/rhasspy/__init__.py b/tests/components/rhasspy/__init__.py new file mode 100644 index 00000000000..521c5166040 --- /dev/null +++ b/tests/components/rhasspy/__init__.py @@ -0,0 +1 @@ +"""Tests for the Rhasspy integration.""" diff --git a/tests/components/rhasspy/test_config_flow.py b/tests/components/rhasspy/test_config_flow.py new file mode 100644 index 00000000000..53c82c0cecd --- /dev/null +++ b/tests/components/rhasspy/test_config_flow.py @@ -0,0 +1,44 @@ +"""Test the Rhasspy config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.rhasspy.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.rhasspy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Rhasspy" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_single_entry(hass: HomeAssistant) -> None: + """Test we only allow single entry.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/rhasspy/test_init.py b/tests/components/rhasspy/test_init.py new file mode 100644 index 00000000000..e4f0b346347 --- /dev/null +++ b/tests/components/rhasspy/test_init.py @@ -0,0 +1,26 @@ +"""Tests for the Rhasspy integration.""" +from homeassistant.components.rhasspy.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry(hass: HomeAssistant) -> None: + """Test the Rhasspy configuration entry loading/unloading.""" + mock_config_entry = MockConfigEntry( + title="Rhasspy", + domain=DOMAIN, + data={}, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py index 358ac6783ad..a28660bb7a4 100644 --- a/tests/components/ridwell/test_config_flow.py +++ b/tests/components/ridwell/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.components.ridwell.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_duplicate_error(hass: HomeAssistant, config, config_entry): @@ -20,7 +16,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -39,7 +35,7 @@ async def test_errors(hass: HomeAssistant, config, error, exc) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"]["base"] == error @@ -48,7 +44,7 @@ async def test_show_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -66,7 +62,7 @@ async def test_step_reauth( result["flow_id"], user_input={CONF_PASSWORD: "password"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -76,4 +72,4 @@ async def test_step_user(hass: HomeAssistant, config, setup_ridwell) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 70ec7844624..4a82656147d 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -99,7 +99,7 @@ def two_part_alarm(): "partitions", new_callable=PropertyMock(return_value=partition_mocks), ), patch( - "homeassistant.components.risco.RiscoAPI.get_state", + "homeassistant.components.risco.RiscoCloud.get_state", return_value=alarm_mock, ): yield alarm_mock @@ -109,7 +109,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -125,7 +125,7 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -228,7 +228,7 @@ async def test_states(hass, two_part_alarm): async def _test_service_call( hass, service, method, entity_id, partition_id, *args, **kwargs ): - with patch(f"homeassistant.components.risco.RiscoAPI.{method}") as set_mock: + with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: await _call_alarm_service(hass, service, entity_id, **kwargs) set_mock.assert_awaited_once_with(partition_id, *args) @@ -236,7 +236,7 @@ async def _test_service_call( async def _test_no_service_call( hass, service, method, entity_id, partition_id, **kwargs ): - with patch(f"homeassistant.components.risco.RiscoAPI.{method}") as set_mock: + with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: await _call_alarm_service(hass, service, entity_id, **kwargs) set_mock.assert_not_awaited() @@ -302,10 +302,20 @@ async def test_sets_full_custom_mapping(hass, two_part_alarm): hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C" ) await _test_service_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", FIRST_ENTITY_ID, 0, "D" + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "group_arm", + FIRST_ENTITY_ID, + 0, + "D", ) await _test_service_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", SECOND_ENTITY_ID, 1, "D" + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "group_arm", + SECOND_ENTITY_ID, + 1, + "D", ) @@ -333,10 +343,22 @@ async def test_sets_with_correct_code(hass, two_part_alarm): hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1, **code ) await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, "C", **code + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + FIRST_ENTITY_ID, + 0, + "C", + **code, ) await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C", **code + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + SECOND_ENTITY_ID, + 1, + "C", + **code, ) with pytest.raises(HomeAssistantError): await _test_no_service_call( diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 7f68db7939d..a7c11c9cb00 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -20,7 +20,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -36,7 +36,7 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -106,7 +106,7 @@ async def test_states(hass, two_zone_alarm): # noqa: F811 async def test_bypass(hass, two_zone_alarm): # noqa: F811 """Test bypassing a zone.""" await setup_risco(hass) - with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} await hass.services.async_call( @@ -119,7 +119,7 @@ async def test_bypass(hass, two_zone_alarm): # noqa: F811 async def test_unbypass(hass, two_zone_alarm): # noqa: F811 """Test unbypassing a zone.""" await setup_risco(hass) - with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} await hass.services.async_call( diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index dfd182d4a24..8d04f478e44 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -51,13 +51,13 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", + "homeassistant.components.risco.config_flow.RiscoCloud.login", return_value=True, ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.site_name", + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.close" + "homeassistant.components.risco.config_flow.RiscoCloud.close" ) as mock_close, patch( "homeassistant.components.risco.async_setup_entry", return_value=True, @@ -74,61 +74,33 @@ async def test_form(hass): mock_close.assert_awaited_once() -async def test_form_invalid_auth(hass): - """Test we handle invalid auth.""" +@pytest.mark.parametrize( + "exception, error", + [ + (UnauthorizedError, "invalid_auth"), + (CannotConnectError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_error(hass, exception, error): + """Test we handle config flow errors.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", - side_effect=UnauthorizedError, - ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: + "homeassistant.components.risco.config_flow.RiscoCloud.login", + side_effect=exception, + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ) as mock_close: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} mock_close.assert_awaited_once() - - -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", - side_effect=CannotConnectError, - ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} - mock_close.assert_awaited_once() - - -async def test_form_exception(hass): - """Test we handle unknown exception.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", - side_effect=Exception, - ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} - mock_close.assert_awaited_once() + assert result2["errors"] == {"base": error} async def test_form_already_exists(hass): @@ -165,14 +137,14 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=TEST_OPTIONS, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "risco_to_ha" result = await hass.config_entries.options.async_configure( @@ -180,7 +152,7 @@ async def test_options_flow(hass): user_input=TEST_RISCO_TO_HA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ha_to_risco" with patch("homeassistant.components.risco.async_setup_entry", return_value=True): @@ -189,7 +161,7 @@ async def test_options_flow(hass): user_input=TEST_HA_TO_RISCO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { **TEST_OPTIONS, "risco_states_to_ha": TEST_RISCO_TO_HA, diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 24efbabf087..8fb4daf8624 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -113,7 +113,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -130,7 +130,7 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -175,7 +175,7 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 assert not registry.async_is_registered(id) with patch( - "homeassistant.components.risco.RiscoAPI.site_uuid", + "homeassistant.components.risco.RiscoCloud.site_uuid", new_callable=PropertyMock(return_value=TEST_SITE_UUID), ), patch( "homeassistant.components.risco.Store.async_save", @@ -191,7 +191,7 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 _check_state(hass, category, entity_id) with patch( - "homeassistant.components.risco.RiscoAPI.get_events", return_value=[] + "homeassistant.components.risco.RiscoCloud.get_events", return_value=[] ) as events_mock, patch( "homeassistant.components.risco.Store.async_load", return_value={LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}, diff --git a/tests/components/risco/util.py b/tests/components/risco/util.py index 8b918f32c12..3fa81586d27 100644 --- a/tests/components/risco/util.py +++ b/tests/components/risco/util.py @@ -23,18 +23,18 @@ async def setup_risco(hass, events=[], options={}): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", return_value=True, ), patch( - "homeassistant.components.risco.RiscoAPI.site_uuid", + "homeassistant.components.risco.RiscoCloud.site_uuid", new_callable=PropertyMock(return_value=TEST_SITE_UUID), ), patch( - "homeassistant.components.risco.RiscoAPI.site_name", + "homeassistant.components.risco.RiscoCloud.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.RiscoAPI.close" + "homeassistant.components.risco.RiscoCloud.close" ), patch( - "homeassistant.components.risco.RiscoAPI.get_events", + "homeassistant.components.risco.RiscoCloud.get_events", return_value=events, ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -68,7 +68,7 @@ def two_zone_alarm(): "zones", new_callable=PropertyMock(return_value=zone_mocks), ), patch( - "homeassistant.components.risco.RiscoAPI.get_state", + "homeassistant.components.risco.RiscoCloud.get_state", return_value=alarm_mock, ): yield alarm_mock diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py index 24f92b0b11b..706b1eceddb 100644 --- a/tests/components/roku/test_binary_sensor.py +++ b/tests/components/roku/test_binary_sensor.py @@ -29,7 +29,7 @@ async def test_roku_binary_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_headphones_connected" assert entry.entity_category is None assert state.state == STATE_OFF - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Headphones Connected" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Headphones connected" assert state.attributes.get(ATTR_ICON) == "mdi:headphones" assert ATTR_DEVICE_CLASS not in state.attributes @@ -51,7 +51,7 @@ async def test_roku_binary_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_supports_ethernet" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_ON - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports Ethernet" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports ethernet" assert state.attributes.get(ATTR_ICON) == "mdi:ethernet" assert ATTR_DEVICE_CLASS not in state.attributes @@ -62,7 +62,7 @@ async def test_roku_binary_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_supports_find_remote" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_OFF - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports Find Remote" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports find remote" assert state.attributes.get(ATTR_ICON) == "mdi:remote" assert ATTR_DEVICE_CLASS not in state.attributes @@ -105,7 +105,7 @@ async def test_rokutv_binary_sensors( assert state.state == STATE_OFF assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == '58" Onn Roku TV Headphones Connected' + == '58" Onn Roku TV Headphones connected' ) assert state.attributes.get(ATTR_ICON) == "mdi:headphones" assert ATTR_DEVICE_CLASS not in state.attributes @@ -131,7 +131,7 @@ async def test_rokutv_binary_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_ON assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports Ethernet' + state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports ethernet' ) assert state.attributes.get(ATTR_ICON) == "mdi:ethernet" assert ATTR_DEVICE_CLASS not in state.attributes @@ -147,7 +147,7 @@ async def test_rokutv_binary_sensors( assert state.state == STATE_ON assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == '58" Onn Roku TV Supports Find Remote' + == '58" Onn Roku TV Supports find remote' ) assert state.attributes.get(ATTR_ICON) == "mdi:remote" assert ATTR_DEVICE_CLASS not in state.attributes diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index f5a3d270f70..bac6d7456a3 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.roku import ( @@ -39,7 +35,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" user_input = {CONF_HOST: mock_config_entry.data[CONF_HOST]} @@ -47,7 +43,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) @@ -55,7 +51,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -68,7 +64,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} user_input = {CONF_HOST: HOST} @@ -77,7 +73,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My Roku 3" assert "data" in result @@ -101,7 +97,7 @@ async def test_form_cannot_connect( flow_id=result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -120,7 +116,7 @@ async def test_form_unknown_error( flow_id=result["flow_id"], user_input=user_input ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -137,7 +133,7 @@ async def test_homekit_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -154,7 +150,7 @@ async def test_homekit_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -170,7 +166,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV} @@ -179,7 +175,7 @@ async def test_homekit_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME_ROKUTV assert "data" in result @@ -192,7 +188,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -209,7 +205,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -226,7 +222,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -241,7 +237,7 @@ async def test_ssdp_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} @@ -250,7 +246,7 @@ async def test_ssdp_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == UPNP_FRIENDLY_NAME assert result["data"] diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index de66a1ae3b7..78834ed955a 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -128,7 +128,7 @@ async def test_form_user_discovery_and_password_fetch(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -137,7 +137,7 @@ async def test_form_user_discovery_and_password_fetch(hass): {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -157,7 +157,7 @@ async def test_form_user_discovery_and_password_fetch(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -184,7 +184,7 @@ async def test_form_user_discovery_skips_known(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -204,7 +204,7 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured(ha ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -213,7 +213,7 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured(ha {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -233,7 +233,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -242,7 +242,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -255,7 +255,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] is None with patch( @@ -274,7 +274,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -302,7 +302,7 @@ async def test_form_user_discover_fails_aborts_already_configured(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -311,7 +311,7 @@ async def test_form_user_discover_fails_aborts_already_configured(hass): {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -328,7 +328,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -337,7 +337,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -351,7 +351,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -372,7 +372,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -384,7 +384,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -403,7 +403,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -433,7 +433,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -445,7 +445,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -471,7 +471,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -504,7 +504,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -516,7 +516,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -542,7 +542,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -563,7 +563,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -572,7 +572,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -599,7 +599,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -631,7 +631,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "link" assert result["description_placeholders"] == {"name": "robot_name"} @@ -652,7 +652,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "robot_name" assert result2["result"].unique_id == "BLID" assert result2["data"] == { @@ -684,7 +684,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -693,7 +693,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): {}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -705,7 +705,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] is None with patch( @@ -724,7 +724,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -757,7 +757,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -769,7 +769,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -788,7 +788,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 7cc37bc73cd..3e5bce8c1c2 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -185,7 +185,7 @@ async def test_duplicate_config(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py index 7e302b51512..5c474fc0821 100644 --- a/tests/components/rpi_power/test_config_flow.py +++ b/tests/components/rpi_power/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import MagicMock from homeassistant.components.rpi_power.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import patch @@ -21,13 +17,13 @@ async def test_setup(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert not result["errors"] with patch(MODULE, return_value=MagicMock()): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY async def test_not_supported(hass: HomeAssistant) -> None: @@ -39,7 +35,7 @@ async def test_not_supported(hass: HomeAssistant) -> None: with patch(MODULE, return_value=None): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -50,7 +46,7 @@ async def test_onboarding(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY async def test_onboarding_not_supported(hass: HomeAssistant) -> None: @@ -60,5 +56,5 @@ async def test_onboarding_not_supported(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index bc72dff2535..2c5e9e1ffc9 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_SSL, CONF_URL, ) -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType VALID_CONFIG = { CONF_NAME: "Sabnzbd", @@ -39,7 +39,7 @@ async def test_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -55,7 +55,7 @@ async def test_create_entry(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "edc3eee7330e" assert result2["data"] == { CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", @@ -93,7 +93,7 @@ async def test_import_flow(hass) -> None: data=VALID_CONFIG_OLD, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "edc3eee7330e" assert result["data"][CONF_NAME] == "Sabnzbd" assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0" diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 11aaf12d9ee..0b49a064a19 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -57,11 +57,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry @@ -363,7 +359,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -375,7 +371,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT @@ -447,7 +443,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} with patch( @@ -459,7 +455,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -536,7 +532,7 @@ async def test_ssdp_legacy_not_remote_control_receiver_udn( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -582,7 +578,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -591,7 +587,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_model" @@ -611,7 +607,7 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -743,7 +739,7 @@ async def test_ssdp_encrypted_websocket_not_supported( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -1261,7 +1257,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -1276,7 +1272,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: {}, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT diff --git a/tests/components/season/test_config_flow.py b/tests/components/season/test_config_flow.py index 11ebea8f6d6..9a64bcd140a 100644 --- a/tests/components/season/test_config_flow.py +++ b/tests/components/season/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.season.const import ( from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +25,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -38,7 +34,7 @@ async def test_full_user_flow( user_input={CONF_TYPE: TYPE_ASTRONOMICAL}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Season" assert result2.get("data") == {CONF_TYPE: TYPE_ASTRONOMICAL} @@ -56,7 +52,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source}, data={CONF_TYPE: TYPE_ASTRONOMICAL} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -71,6 +67,6 @@ async def test_import_flow( data={CONF_NAME: "My Seasons", CONF_TYPE: TYPE_METEOROLOGICAL}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "My Seasons" assert result.get("data") == {CONF_TYPE: TYPE_METEOROLOGICAL} diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 90d01106bba..0c2470edb7b 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -4,7 +4,11 @@ from datetime import datetime from freezegun import freeze_time import pytest -from homeassistant.components.season.const import TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL +from homeassistant.components.season.const import ( + DOMAIN, + TYPE_ASTRONOMICAL, + TYPE_METEOROLOGICAL, +) from homeassistant.components.season.sensor import ( STATE_AUTUMN, STATE_SPRING, @@ -13,7 +17,7 @@ from homeassistant.components.season.sensor import ( ) from homeassistant.const import CONF_TYPE, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -123,6 +127,14 @@ async def test_season_southern_hemisphere( assert entry assert entry.unique_id == mock_config_entry.entry_id + device_registry = dr.async_get(hass) + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, mock_config_entry.entry_id)} + assert device_entry.name == "Season" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + async def test_season_equator( hass: HomeAssistant, diff --git a/tests/components/senseme/test_config_flow.py b/tests/components/senseme/test_config_flow.py index e85845dcace..63850dff3b5 100644 --- a/tests/components/senseme/test_config_flow.py +++ b/tests/components/senseme/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components import dhcp from homeassistant.components.senseme.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_ADDRESS, @@ -42,7 +38,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -53,7 +49,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Haiku Fan" assert result2["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -68,7 +64,7 @@ async def test_form_user_manual_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -79,7 +75,7 @@ async def test_form_user_manual_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual" with patch( @@ -97,7 +93,7 @@ async def test_form_user_manual_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Haiku Fan" assert result3["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -118,7 +114,7 @@ async def test_form_user_no_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -129,7 +125,7 @@ async def test_form_user_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual" assert result2["errors"] == {CONF_HOST: "invalid_host"} @@ -141,7 +137,7 @@ async def test_form_user_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Haiku Fan" assert result3["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -156,7 +152,7 @@ async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -167,7 +163,7 @@ async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual" with patch( @@ -182,7 +178,7 @@ async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["step_id"] == "manual" assert result3["errors"] == {CONF_HOST: "cannot_connect"} @@ -214,7 +210,7 @@ async def test_discovery(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: MOCK_UUID}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -225,7 +221,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Haiku Fan" assert result2["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -257,7 +253,7 @@ async def test_discovery_existing_device_no_ip_change(hass: HomeAssistant) -> No context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: MOCK_UUID}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -286,7 +282,7 @@ async def test_discovery_existing_device_ip_change(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["info"]["address"] == "127.0.0.8" @@ -304,7 +300,7 @@ async def test_dhcp_discovery_existing_config_entry(hass: HomeAssistant) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -320,7 +316,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -331,7 +327,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Haiku Fan" assert result2["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -348,7 +344,7 @@ async def test_dhcp_discovery_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -361,5 +357,5 @@ async def test_dhcp_discovery_cannot_connect_no_uuid(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index 7e92e1f2eb3..509fe8633d7 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -12,11 +12,7 @@ import pytest from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -51,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["version"] == 2 assert result2["data"] == { "api_key": "1234567890", @@ -78,7 +74,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -111,7 +107,7 @@ async def test_flow_fails( }, ) - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Sensibo" assert result3["data"] == { "api_key": "1234567891", @@ -125,7 +121,7 @@ async def test_flow_get_no_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -152,7 +148,7 @@ async def test_flow_get_no_username(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -192,7 +188,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -211,7 +207,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -261,7 +257,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -280,7 +276,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -330,7 +326,7 @@ async def test_flow_reauth_no_username_or_device( data=entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -349,5 +345,5 @@ async def test_flow_reauth_no_username_or_device( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 0bae8235ff9..3a593b0e6cc 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -107,7 +107,7 @@ async def test_deprecated_last_reset( f"with state_class {state_class} has set last_reset. Setting last_reset for " "entities with state_class other than 'total' is not supported. Please update " "your configuration if state_class is manually configured, otherwise report it " - "to the custom component author." + "to the custom integration author." ) in caplog.text state = hass.states.get("sensor.test") diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index c62d1309c7a..cc2f9c76f1f 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -10,7 +10,6 @@ from pytest import approx from homeassistant import loader from homeassistant.components.recorder import history -from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.db_schema import StatisticsMeta from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( @@ -18,7 +17,7 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, statistics_during_period, ) -from homeassistant.components.recorder.util import session_scope +from homeassistant.components.recorder.util import get_instance, session_scope from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util @@ -775,7 +774,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( ( "sensor.custom_sensor", "from integration test ", - "report it to the custom component author", + "report it to the custom integration author", ), ], ) @@ -2290,7 +2289,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): hass = hass_recorder() # Remove this after dropping the use of the hass_recorder fixture hass.config.set_time_zone("America/Regina") - recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -2454,7 +2453,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): sum_adjustement_start = zero + timedelta(minutes=65) for i in range(13, 24): expected_sums["sensor.test4"][i] += sum_adjustment - recorder.async_adjust_statistics( + instance.async_adjust_statistics( "sensor.test4", sum_adjustement_start, sum_adjustment ) wait_recording_done(hass) diff --git a/tests/components/sensorpush/__init__.py b/tests/components/sensorpush/__init__.py new file mode 100644 index 00000000000..0fe9ced64df --- /dev/null +++ b/tests/components/sensorpush/__init__.py @@ -0,0 +1,34 @@ +"""Tests for the SensorPush integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +HTW_SERVICE_INFO = BluetoothServiceInfo( + name="SensorPush HT.w 0CA1", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={11271: b"\xfe\x00\x01"}, + service_data={}, + service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090ab0"], + source="local", +) + +HTPWX_SERVICE_INFO = BluetoothServiceInfo( + name="SensorPush HTP.xw F4D", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={7168: b"\xcd=!\xd1\xb9"}, + service_data={}, + service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090ab0"], + source="local", +) diff --git a/tests/components/sensorpush/conftest.py b/tests/components/sensorpush/conftest.py new file mode 100644 index 00000000000..2a983a7a4ed --- /dev/null +++ b/tests/components/sensorpush/conftest.py @@ -0,0 +1,8 @@ +"""SensorPush session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py new file mode 100644 index 00000000000..1c825640603 --- /dev/null +++ b/tests/components/sensorpush/test_config_flow.py @@ -0,0 +1,170 @@ +"""Test the SensorPush config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.sensorpush.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import HTPWX_SERVICE_INFO, HTW_SERVICE_INFO, NOT_SENSOR_PUSH_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTPWX_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "HTP.xw F4D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_bluetooth_not_sensorpush(hass): + """Test discovery via bluetooth not sensorpush.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_SENSOR_PUSH_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "HT.w 0CA1" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "HT.w 0CA1" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py new file mode 100644 index 00000000000..34179985d78 --- /dev/null +++ b/tests/components/sensorpush/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the SensorPush config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.sensorpush.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import HTPWX_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(HTPWX_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.htp_xw_f4d_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "20.11" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "HTP.xw F4D Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 77dd440a2da..984c486c69e 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -17,11 +17,7 @@ from homeassistant.components.sentry.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -32,7 +28,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert "flow_id" in result @@ -62,7 +58,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -82,7 +78,7 @@ async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": "bad_dsn"} @@ -102,7 +98,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": "unknown"} @@ -120,7 +116,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -138,7 +134,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("data") == { CONF_ENVIRONMENT: "Test", CONF_EVENT_CUSTOM_COMPONENTS: True, diff --git a/tests/components/senz/test_config_flow.py b/tests/components/senz/test_config_flow.py index 3906f2c0320..f0ed54ba932 100644 --- a/tests/components/senz/test_config_flow.py +++ b/tests/components/senz/test_config_flow.py @@ -3,10 +3,15 @@ from unittest.mock import patch from aiosenz import AUTHORIZATION_ENDPOINT, TOKEN_ENDPOINT -from homeassistant import config_entries, setup +from homeassistant import config_entries +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.senz.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -19,12 +24,11 @@ async def test_full_flow( current_request_with_host, ) -> None: """Check full flow.""" - assert await setup.async_setup_component( - hass, - "senz", - { - "senz": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, - }, + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "cred" ) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 145bcbb3566..f47fdef0994 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -75,7 +75,7 @@ async def test_form(hass, gen): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -93,7 +93,7 @@ async def test_title_without_name(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} settings = MOCK_SETTINGS.copy() @@ -123,7 +123,7 @@ async def test_title_without_name(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "shelly1pm-12345" assert result2["data"] == { "host": "1.1.1.1", @@ -148,7 +148,7 @@ async def test_form_auth(hass, test_data): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -160,7 +160,7 @@ async def test_form_auth(hass, test_data): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -191,7 +191,7 @@ async def test_form_auth(hass, test_data): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", @@ -221,7 +221,7 @@ async def test_form_errors_get_info(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -248,7 +248,7 @@ async def test_form_missing_model_key(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -257,7 +257,7 @@ async def test_form_missing_model_key_auth_enabled(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -269,7 +269,7 @@ async def test_form_missing_model_key_auth_enabled(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -286,7 +286,7 @@ async def test_form_missing_model_key_auth_enabled(hass): result2["flow_id"], {"password": "1234"} ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -311,14 +311,14 @@ async def test_form_missing_model_key_zeroconf(hass, caplog): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "firmware_not_fully_provisioned"} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -342,7 +342,7 @@ async def test_form_errors_test_connection(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -367,7 +367,7 @@ async def test_form_already_configured(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -416,7 +416,7 @@ async def test_user_setup_ignored_device(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Test config entry got updated with latest IP assert entry.data["host"] == "1.1.1.1" @@ -439,7 +439,7 @@ async def test_form_firmware_unsupported(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "unsupported_firmware" @@ -482,7 +482,7 @@ async def test_form_auth_errors_test_connection_gen1(hass, error): result2["flow_id"], {"username": "test username", "password": "test password"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -524,7 +524,7 @@ async def test_form_auth_errors_test_connection_gen2(hass, error): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "test password"} ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -548,7 +548,7 @@ async def test_zeroconf(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -569,7 +569,7 @@ async def test_zeroconf(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -614,7 +614,7 @@ async def test_zeroconf_sleeping_device(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -634,7 +634,7 @@ async def test_zeroconf_sleeping_device(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -677,7 +677,7 @@ async def test_zeroconf_sleeping_device_error(hass, error): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -698,7 +698,7 @@ async def test_zeroconf_already_configured(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -717,7 +717,7 @@ async def test_zeroconf_firmware_unsupported(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unsupported_firmware" @@ -729,7 +729,7 @@ async def test_zeroconf_cannot_connect(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -745,7 +745,7 @@ async def test_zeroconf_require_auth(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -768,7 +768,7 @@ async def test_zeroconf_require_auth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", diff --git a/tests/components/shopping_list/test_config_flow.py b/tests/components/shopping_list/test_config_flow.py index dfc23e18504..552bf19bcd1 100644 --- a/tests/components/shopping_list/test_config_flow.py +++ b/tests/components/shopping_list/test_config_flow.py @@ -11,7 +11,7 @@ async def test_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_user(hass): @@ -21,7 +21,7 @@ async def test_user(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -32,5 +32,5 @@ async def test_user_confirm(hass): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].data == {} diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index 6b1517601f4..85992d0d811 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -160,7 +160,9 @@ async def test_form_start_account(hass, flow_at_add_account_step): async def test_create(hass, entry_with_basic_config): """Test we create a entry through the form.""" - assert entry_with_basic_config["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + entry_with_basic_config["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + ) assert ( entry_with_basic_config["title"] == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" @@ -173,7 +175,7 @@ async def test_create_additional_account(hass, entry_with_additional_account_con """Test we create a config with two accounts.""" assert ( entry_with_additional_account_config["type"] - == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + == data_entry_flow.FlowResultType.CREATE_ENTRY ) assert ( entry_with_additional_account_config["title"] @@ -314,14 +316,14 @@ async def test_options_basic(hass): ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" assert result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], BASIC_OPTIONS ) - assert updated["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert updated["data"] == { CONF_ACCOUNTS: {BASIC_CONFIG[CONF_ACCOUNT]: BASIC_OPTIONS} } @@ -339,13 +341,13 @@ async def test_options_additional(hass): ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" assert not result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], BASIC_OPTIONS ) - assert updated["type"] == data_entry_flow.RESULT_TYPE_FORM + assert updated["type"] == data_entry_flow.FlowResultType.FORM assert updated["step_id"] == "options" assert updated["last_step"] diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py index 4636df6b28f..6c37fb7ffe6 100644 --- a/tests/components/simplepush/test_config_flow.py +++ b/tests/components/simplepush/test_config_flow.py @@ -45,7 +45,7 @@ async def test_flow_successful(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == MOCK_CONFIG @@ -61,7 +61,7 @@ async def test_flow_with_password(hass: HomeAssistant) -> None: result["flow_id"], user_input=mock_config_pass, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == mock_config_pass @@ -84,7 +84,7 @@ async def test_flow_user_device_key_already_configured(hass: HomeAssistant) -> N result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -109,7 +109,7 @@ async def test_flow_user_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_error_on_connection_failure(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -139,6 +139,6 @@ async def test_flow_import(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == MOCK_CONFIG diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 4cb248cdbd0..6e6f99ad4bb 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -55,6 +55,7 @@ async def test_options_flow(config_entry, hass): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index 0171a522e50..cd2b5053ac7 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -6,11 +6,7 @@ from aioskybell import exceptions from homeassistant.components.skybell.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import CONF_CONFIG_FLOW, _patch_skybell, _patch_skybell_devices @@ -38,7 +34,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -46,7 +42,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_CONFIG_FLOW @@ -64,7 +60,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -75,7 +71,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -88,7 +84,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -100,7 +96,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -115,7 +111,7 @@ async def test_flow_import(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_CONFIG_FLOW @@ -133,5 +129,5 @@ async def test_flow_import_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_IMPORT}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/slack/test_config_flow.py b/tests/components/slack/test_config_flow.py index 850690783e8..97c8e6ee743 100644 --- a/tests/components/slack/test_config_flow.py +++ b/tests/components/slack/test_config_flow.py @@ -23,7 +23,7 @@ async def test_flow_user( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -42,7 +42,7 @@ async def test_flow_user_already_configured( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -56,7 +56,7 @@ async def test_flow_user_invalid_auth( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -71,7 +71,7 @@ async def test_flow_user_cannot_connect( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -87,7 +87,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -103,7 +103,7 @@ async def test_flow_import( data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -119,7 +119,7 @@ async def test_flow_import_no_name( data=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -136,5 +136,5 @@ async def test_flow_import_already_configured( data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index 3101a7ecdfe..75a2524e2bc 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -46,7 +46,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -67,7 +67,7 @@ async def test_login_failure(hass: HomeAssistant, side_effect, error) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error} @@ -90,7 +90,7 @@ async def test_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] assert len(mock_setup_entry.mock_calls) == 1 @@ -128,5 +128,5 @@ async def test_reauth_password(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/slimproto/test_config_flow.py b/tests/components/slimproto/test_config_flow.py index 0c0c843f0b5..15ea5434fc5 100644 --- a/tests/components/slimproto/test_config_flow.py +++ b/tests/components/slimproto/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock from homeassistant.components.slimproto.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == DEFAULT_NAME assert result.get("data") == {} @@ -34,5 +34,5 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index 8cf22b3634e..eeaa0d75f07 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -9,11 +9,7 @@ from pysma.exceptions import ( from homeassistant.components.sma.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_DEVICE, MOCK_USER_INPUT, _patch_async_setup_entry @@ -24,7 +20,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch("pysma.SMA.new_session", return_value=True), patch( @@ -36,7 +32,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_USER_INPUT["host"] assert result["data"] == MOCK_USER_INPUT @@ -57,7 +53,7 @@ async def test_form_cannot_connect(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -76,7 +72,7 @@ async def test_form_invalid_auth(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} assert len(mock_setup_entry.mock_calls) == 0 @@ -95,7 +91,7 @@ async def test_form_cannot_retrieve_device_info(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve_device_info"} assert len(mock_setup_entry.mock_calls) == 0 @@ -114,7 +110,7 @@ async def test_form_unexpected_exception(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} assert len(mock_setup_entry.mock_calls) == 0 @@ -138,6 +134,6 @@ async def test_form_already_configured(hass, mock_config_entry): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 16d330a21a8..b29207afbca 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_user_form(hass): ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_show_user_host_form(hass): @@ -39,14 +39,14 @@ async def test_show_user_host_form(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_show_zeroconf_connection_error_form(hass): @@ -67,14 +67,14 @@ async def test_show_zeroconf_connection_error_form(hass): ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -97,14 +97,14 @@ async def test_show_zeroconf_connection_error_form_next_generation(hass): ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -119,19 +119,19 @@ async def test_connection_error(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_user_local_connection_error(hass): @@ -148,19 +148,19 @@ async def test_user_local_connection_error(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_zeroconf_wrong_mdns(hass): @@ -180,7 +180,7 @@ async def test_zeroconf_wrong_mdns(hass): ) assert result["reason"] == "invalid_mdns" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_full_user_wrong_mdns(hass): @@ -199,18 +199,18 @@ async def test_full_user_wrong_mdns(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_mdns" @@ -239,18 +239,18 @@ async def test_user_device_exists_abort(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -289,7 +289,7 @@ async def test_zeroconf_device_exists_abort(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -310,7 +310,7 @@ async def test_cloud_device_exists_abort(hass): context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -339,7 +339,7 @@ async def test_zeroconf_abort_if_cloud_device_exists(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -371,7 +371,7 @@ async def test_zeroconf_confirm_abort_if_cloud_device_exists(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -396,7 +396,7 @@ async def test_abort_cloud_flow_if_local_device_exists(hass): result["flow_id"], {"environment": ENV_CLOUD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_local_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -479,7 +479,7 @@ async def test_full_zeroconf_flow(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} @@ -487,7 +487,7 @@ async def test_full_zeroconf_flow(hass): result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -513,7 +513,7 @@ async def test_full_user_local_flow(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] is None result = await hass.config_entries.flow.async_configure( @@ -521,12 +521,12 @@ async def test_full_user_local_flow(hass): {"environment": ENV_LOCAL}, ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -555,7 +555,7 @@ async def test_full_zeroconf_flow_next_generation(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} @@ -563,7 +563,7 @@ async def test_full_zeroconf_flow_next_generation(hass): result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "smappee5001000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 420c07d2a04..93b646c44dc 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -27,7 +27,7 @@ async def test_import_shows_user_step(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -52,7 +52,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -60,7 +60,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -69,14 +69,14 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -85,7 +85,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -123,7 +123,7 @@ async def test_entry_created_from_update_event( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -131,7 +131,7 @@ async def test_entry_created_from_update_event( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -140,14 +140,14 @@ async def test_entry_created_from_update_event( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -156,7 +156,7 @@ async def test_entry_created_from_update_event( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -194,7 +194,7 @@ async def test_entry_created_existing_app_new_oauth_client( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -202,7 +202,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -211,14 +211,14 @@ async def test_entry_created_existing_app_new_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -227,7 +227,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -278,7 +278,7 @@ async def test_entry_created_existing_app_copies_oauth_client( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -286,7 +286,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -297,14 +297,14 @@ async def test_entry_created_existing_app_copies_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -313,7 +313,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -370,7 +370,7 @@ async def test_entry_created_with_cloudhook( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -379,7 +379,7 @@ async def test_entry_created_with_cloudhook( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -388,14 +388,14 @@ async def test_entry_created_with_cloudhook( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -404,7 +404,7 @@ async def test_entry_created_with_cloudhook( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -432,7 +432,7 @@ async def test_invalid_webhook_aborts(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_webhook_url" assert result["description_placeholders"][ "webhook_url" @@ -448,7 +448,7 @@ async def test_invalid_token_shows_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -456,7 +456,7 @@ async def test_invalid_token_shows_error(hass): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -465,7 +465,7 @@ async def test_invalid_token_shows_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_invalid_format"} @@ -485,7 +485,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -493,7 +493,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -502,7 +502,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_unauthorized"} @@ -522,7 +522,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -530,7 +530,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -539,7 +539,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_forbidden"} @@ -565,7 +565,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -573,7 +573,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -582,7 +582,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "webhook_error"} @@ -607,7 +607,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -615,7 +615,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -624,7 +624,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -645,7 +645,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -653,7 +653,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -662,7 +662,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -679,7 +679,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -687,7 +687,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -696,7 +696,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -721,7 +721,7 @@ async def test_no_available_locations_aborts( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -729,7 +729,7 @@ async def test_no_available_locations_aborts( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -738,5 +738,5 @@ async def test_no_available_locations_aborts( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_available_locations" diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index c6170afc30e..c388f17c3ba 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -73,14 +73,14 @@ async def test_reauth_success(hass, smarttub_api, account): data=mock_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_EMAIL: "test-email3", CONF_PASSWORD: "test-password3"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_entry.data[CONF_EMAIL] == "test-email3" assert mock_entry.data[CONF_PASSWORD] == "test-password3" @@ -114,12 +114,12 @@ async def test_reauth_wrong_account(hass, smarttub_api, account): data=mock_entry2.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index d0f0ac01235..c23d2578d1c 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -31,7 +31,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" # test with all provided @@ -40,7 +40,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == "solaredge_site_1_2_3" data = result.get("data") @@ -62,7 +62,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> Non context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "already_configured"} @@ -82,7 +82,7 @@ async def test_ignored_entry_does_not_cause_error( context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test" data = result["data"] @@ -102,7 +102,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "site_not_active"} # test with api_failure @@ -112,7 +112,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout @@ -122,7 +122,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} # test with HTTPError @@ -132,5 +132,5 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index ccbc5412562..765f1f569e6 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -63,12 +63,12 @@ async def test_user(hass, test_connect): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # tets with all provided result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -79,19 +79,19 @@ async def test_import(hass, test_connect): # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog" assert result["data"][CONF_HOST] == HOST # import with only name result = await flow.async_step_import({CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == DEFAULT_HOST # import with host and name result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -107,19 +107,19 @@ async def test_abort_if_already_setup(hass, test_connect): result = await flow.async_step_import( {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same HOST and NAME result = await flow.async_step_user({CONF_HOST: HOST, CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "already_configured"} # SHOULD pass, diff HOST (without http://), different NAME result = await flow.async_step_import( {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_7_8_9" assert result["data"][CONF_HOST] == "http://2.2.2.2" @@ -127,6 +127,6 @@ async def test_abort_if_already_setup(hass, test_connect): result = await flow.async_step_import( {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == "http://2.2.2.2" diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 1d00f83a608..66d16ebc480 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): flow = config_flow.SomaFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_import_abort(hass): @@ -27,7 +27,7 @@ async def test_import_abort(hass): flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -37,7 +37,7 @@ async def test_import_create(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_error_status(hass): @@ -46,7 +46,7 @@ async def test_error_status(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "error"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "result_error" @@ -56,7 +56,7 @@ async def test_key_error(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -66,7 +66,7 @@ async def test_exception(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", side_effect=RequestException()): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -77,4 +77,4 @@ async def test_full_flow(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_user({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index d98e7429d8b..1fbb55ca864 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -175,7 +175,7 @@ async def test_options_not_loaded(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT @pytest.mark.parametrize("reversed", [True, False]) @@ -204,7 +204,7 @@ async def test_options_with_targets(hass, reversed): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -212,19 +212,19 @@ async def test_options_with_targets(hass, reversed): user_input={"target_id": "a"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={"reverse": reversed}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM result4 = await hass.config_entries.options.async_configure( result3["flow_id"], user_input={"target_id": None}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_REVERSED_TARGET_IDS: {"a": reversed}, diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 59783995d23..5eea0974dee 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -13,11 +13,7 @@ from homeassistant.components.sonarr.const import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.sonarr import MOCK_REAUTH_INPUT, MOCK_USER_INPUT @@ -31,7 +27,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_cannot_connect( @@ -47,7 +43,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -67,7 +63,7 @@ async def test_invalid_auth( data=user_input, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -85,7 +81,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -108,14 +104,14 @@ async def test_full_reauth_flow_implementation( data=entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_REAUTH_INPUT.copy() @@ -124,7 +120,7 @@ async def test_full_reauth_flow_implementation( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "test-api-key-reauth" @@ -141,7 +137,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -151,7 +147,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -168,7 +164,7 @@ async def test_full_user_flow_advanced_options( DOMAIN, context={CONF_SOURCE: SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -181,7 +177,7 @@ async def test_full_user_flow_advanced_options( user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -203,7 +199,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -212,6 +208,6 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_UPCOMING_DAYS] == 2 assert result["data"][CONF_WANTED_MAX_ITEMS] == 100 diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 4d58639a1d0..85293d37d9c 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components import ssdp from homeassistant.components.songpal.const import CONF_ENDPOINT, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_DATA, @@ -79,7 +75,7 @@ async def test_flow_ssdp(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -93,7 +89,7 @@ async def test_flow_user(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None _flow_next(hass, result["flow_id"]) @@ -102,7 +98,7 @@ async def test_flow_user(hass): result["flow_id"], user_input={CONF_ENDPOINT: ENDPOINT}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == { CONF_NAME: MODEL, @@ -121,7 +117,7 @@ async def test_flow_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -137,7 +133,7 @@ async def test_flow_import_without_name(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_ENDPOINT: ENDPOINT} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == {CONF_NAME: MODEL, CONF_ENDPOINT: ENDPOINT} @@ -165,7 +161,7 @@ async def test_ssdp_bravia(hass): context={"source": SOURCE_SSDP}, data=ssdp_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_songpal_device" @@ -177,7 +173,7 @@ async def test_sddp_exist(hass): context={"source": SOURCE_SSDP}, data=SSDP_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -190,7 +186,7 @@ async def test_user_exist(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -206,7 +202,7 @@ async def test_import_exist(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -222,7 +218,7 @@ async def test_user_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -239,7 +235,7 @@ async def test_import_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" mocked_device.get_supported_methods.assert_called_once() diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 02897c523c1..3b7c86478e0 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -26,10 +26,10 @@ async def test_creating_entry_sets_up_media_player( ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 425b11fc35c..9c2119c8331 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,25 +1,8 @@ """Tests for the Sonos Media Player platform.""" -import pytest - -from homeassistant.components.sonos import DOMAIN, media_player from homeassistant.const import STATE_IDLE -from homeassistant.core import Context -from homeassistant.exceptions import Unauthorized from homeassistant.helpers import device_registry as dr -async def test_services(hass, async_autosetup_sonos, hass_read_only_user): - """Test join/unjoin requires control access.""" - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - media_player.SERVICE_JOIN, - {"master": "media_player.bla", "entity_id": "media_player.blub"}, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) - - async def test_device_registry(hass, async_autosetup_sonos, soco): """Test sonos device registered in the device registry.""" device_registry = dr.async_get(hass) diff --git a/tests/components/sonos/test_services.py b/tests/components/sonos/test_services.py new file mode 100644 index 00000000000..d0bf3bf3a06 --- /dev/null +++ b/tests/components/sonos/test_services.py @@ -0,0 +1,43 @@ +"""Tests for Sonos services.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.media_player.const import SERVICE_JOIN +from homeassistant.components.sonos.const import DATA_SONOS +from homeassistant.exceptions import HomeAssistantError + + +async def test_media_player_join(hass, async_autosetup_sonos): + """Test join service.""" + valid_entity_id = "media_player.zone_a" + mocked_entity_id = "media_player.mocked" + + # Ensure an error is raised if the entity is unknown + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + MP_DOMAIN, + SERVICE_JOIN, + {"entity_id": valid_entity_id, "group_members": mocked_entity_id}, + blocking=True, + ) + + # Ensure SonosSpeaker.join_multi is called if entity is found + mocked_speaker = Mock() + mock_entity_id_mappings = {mocked_entity_id: mocked_speaker} + + with patch.dict( + hass.data[DATA_SONOS].entity_id_mappings, mock_entity_id_mappings + ), patch( + "homeassistant.components.sonos.speaker.SonosSpeaker.join_multi" + ) as mock_join_multi: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_JOIN, + {"entity_id": valid_entity_id, "group_members": mocked_entity_id}, + blocking=True, + ) + + found_speaker = hass.data[DATA_SONOS].entity_id_mappings[valid_entity_id] + mock_join_multi.assert_called_with(hass, found_speaker, [mocked_speaker]) diff --git a/tests/components/soundtouch/conftest.py b/tests/components/soundtouch/conftest.py index dcac360d253..21de9e2ed47 100644 --- a/tests/components/soundtouch/conftest.py +++ b/tests/components/soundtouch/conftest.py @@ -4,9 +4,9 @@ from requests_mock import Mocker from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.soundtouch.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PLATFORM +from homeassistant.const import CONF_HOST, CONF_NAME -from tests.common import load_fixture +from tests.common import MockConfigEntry, load_fixture DEVICE_1_ID = "020000000001" DEVICE_2_ID = "020000000002" @@ -14,8 +14,8 @@ DEVICE_1_IP = "192.168.42.1" DEVICE_2_IP = "192.168.42.2" DEVICE_1_URL = f"http://{DEVICE_1_IP}:8090" DEVICE_2_URL = f"http://{DEVICE_2_IP}:8090" -DEVICE_1_NAME = "My Soundtouch 1" -DEVICE_2_NAME = "My Soundtouch 2" +DEVICE_1_NAME = "My SoundTouch 1" +DEVICE_2_NAME = "My SoundTouch 2" DEVICE_1_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_1" DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2" @@ -24,15 +24,29 @@ DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2" @pytest.fixture -def device1_config() -> dict[str, str]: - """Mock SoundTouch device 1 config.""" - yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_1_IP, CONF_NAME: DEVICE_1_NAME} +def device1_config() -> MockConfigEntry: + """Mock SoundTouch device 1 config entry.""" + yield MockConfigEntry( + domain=DOMAIN, + unique_id=DEVICE_1_ID, + data={ + CONF_HOST: DEVICE_1_IP, + CONF_NAME: "", + }, + ) @pytest.fixture -def device2_config() -> dict[str, str]: - """Mock SoundTouch device 2 config.""" - yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_2_IP, CONF_NAME: DEVICE_2_NAME} +def device2_config() -> MockConfigEntry: + """Mock SoundTouch device 2 config entry.""" + yield MockConfigEntry( + domain=DOMAIN, + unique_id=DEVICE_2_ID, + data={ + CONF_HOST: DEVICE_2_IP, + CONF_NAME: "", + }, + ) @pytest.fixture(scope="session") diff --git a/tests/components/soundtouch/test_config_flow.py b/tests/components/soundtouch/test_config_flow.py new file mode 100644 index 00000000000..cbeb27be979 --- /dev/null +++ b/tests/components/soundtouch/test_config_flow.py @@ -0,0 +1,113 @@ +"""Test config flow.""" +from unittest.mock import patch + +from requests import RequestException +from requests_mock import ANY, Mocker + +from homeassistant.components.soundtouch.const import DOMAIN +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import DEVICE_1_ID, DEVICE_1_IP, DEVICE_1_NAME + + +async def test_user_flow_create_entry( + hass: HomeAssistant, device1_requests_mock_standby: Mocker +) -> None: + """Test the full manual user flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + assert "flow_id" in result + + with patch( + "homeassistant.components.soundtouch.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: DEVICE_1_IP, + }, + ) + + assert len(mock_setup_entry.mock_calls) == 1 + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == DEVICE_1_NAME + assert result.get("data") == { + CONF_HOST: DEVICE_1_IP, + } + assert "result" in result + assert result["result"].unique_id == DEVICE_1_ID + assert result["result"].title == DEVICE_1_NAME + + +async def test_user_flow_cannot_connect( + hass: HomeAssistant, requests_mock: Mocker +) -> None: + """Test a manual user flow with an invalid host.""" + requests_mock.get(ANY, exc=RequestException()) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={ + CONF_HOST: "invalid-hostname", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_zeroconf_flow_create_entry( + hass: HomeAssistant, device1_requests_mock_standby: Mocker +) -> None: + """Test the zeroconf flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + host=DEVICE_1_IP, + addresses=[DEVICE_1_IP], + port=8090, + hostname="Bose-SM2-060000000001.local.", + type="_soundtouch._tcp.local.", + name=f"{DEVICE_1_NAME}._soundtouch._tcp.local.", + properties={ + "DESCRIPTION": "SoundTouch", + "MAC": DEVICE_1_ID, + "MANUFACTURER": "Bose Corporation", + "MODEL": "SoundTouch", + }, + ), + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "zeroconf_confirm" + assert result.get("description_placeholders") == {"name": DEVICE_1_NAME} + + with patch( + "homeassistant.components.soundtouch.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert len(mock_setup_entry.mock_calls) == 1 + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == DEVICE_1_NAME + assert result.get("data") == { + CONF_HOST: DEVICE_1_IP, + } + assert "result" in result + assert result["result"].unique_id == DEVICE_1_ID + assert result["result"].title == DEVICE_1_NAME diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 1b16508bb88..5105d07479c 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -1,4 +1,5 @@ """Test the SoundTouch component.""" +from datetime import timedelta from typing import Any from requests_mock import Mocker @@ -25,22 +26,26 @@ from homeassistant.components.soundtouch.const import ( from homeassistant.components.soundtouch.media_player import ( ATTR_SOUNDTOUCH_GROUP, ATTR_SOUNDTOUCH_ZONE, - DATA_SOUNDTOUCH, ) +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt from .conftest import DEVICE_1_ENTITY_ID, DEVICE_2_ENTITY_ID +from tests.common import MockConfigEntry, async_fire_time_changed -async def setup_soundtouch(hass: HomeAssistant, *configs: dict[str, str]): + +async def setup_soundtouch(hass: HomeAssistant, *mock_entries: MockConfigEntry): """Initialize media_player for tests.""" - assert await async_setup_component( - hass, MEDIA_PLAYER_DOMAIN, {MEDIA_PLAYER_DOMAIN: list(configs)} - ) + assert await async_setup_component(hass, MEDIA_PLAYER_DOMAIN, {}) + + for mock_entry in mock_entries: + mock_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - await hass.async_start() async def _test_key_service( @@ -59,7 +64,7 @@ async def _test_key_service( async def test_playing_media( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, ): """Test playing media info.""" @@ -76,7 +81,7 @@ async def test_playing_media( async def test_playing_radio( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_radio, ): """Test playing radio info.""" @@ -89,7 +94,7 @@ async def test_playing_radio( async def test_playing_aux( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_aux, ): """Test playing AUX info.""" @@ -102,7 +107,7 @@ async def test_playing_aux( async def test_playing_bluetooth( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_bluetooth, ): """Test playing Bluetooth info.""" @@ -118,7 +123,7 @@ async def test_playing_bluetooth( async def test_get_volume_level( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, ): """Test volume level.""" @@ -130,7 +135,7 @@ async def test_get_volume_level( async def test_get_state_off( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, ): """Test state device is off.""" @@ -142,7 +147,7 @@ async def test_get_state_off( async def test_get_state_pause( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp_paused, ): """Test state device is paused.""" @@ -154,7 +159,7 @@ async def test_get_state_pause( async def test_is_muted( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_volume_muted: str, ): @@ -170,7 +175,7 @@ async def test_is_muted( async def test_should_turn_off( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -187,7 +192,7 @@ async def test_should_turn_off( async def test_should_turn_on( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_key, ): @@ -204,7 +209,7 @@ async def test_should_turn_on( async def test_volume_up( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -221,7 +226,7 @@ async def test_volume_up( async def test_volume_down( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -238,7 +243,7 @@ async def test_volume_down( async def test_set_volume_level( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_volume, ): @@ -258,7 +263,7 @@ async def test_set_volume_level( async def test_mute( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -275,7 +280,7 @@ async def test_mute( async def test_play( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp_paused, device1_requests_mock_key, ): @@ -292,7 +297,7 @@ async def test_play( async def test_pause( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -309,7 +314,7 @@ async def test_pause( async def test_play_pause( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -326,7 +331,7 @@ async def test_play_pause( async def test_next_previous_track( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -351,7 +356,7 @@ async def test_next_previous_track( async def test_play_media( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -391,7 +396,7 @@ async def test_play_media( async def test_play_media_url( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_dlna, ): @@ -415,7 +420,7 @@ async def test_play_media_url( async def test_select_source_aux( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -435,7 +440,7 @@ async def test_select_source_aux( async def test_select_source_bluetooth( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -455,7 +460,7 @@ async def test_select_source_bluetooth( async def test_select_source_invalid_source( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -477,14 +482,25 @@ async def test_select_source_invalid_source( async def test_play_everywhere( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_set_zone, ): """Test play everywhere.""" - await setup_soundtouch(hass, device1_config, device2_config) + await setup_soundtouch(hass, device1_config) + + # no slaves, set zone must not be called + await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_EVERYWHERE, + {"master": DEVICE_1_ENTITY_ID}, + True, + ) + assert device1_requests_mock_set_zone.call_count == 0 + + await setup_soundtouch(hass, device2_config) # one master, one slave => set zone await hass.services.async_call( @@ -504,27 +520,11 @@ async def test_play_everywhere( ) assert device1_requests_mock_set_zone.call_count == 1 - # remove second device - for entity in list(hass.data[DATA_SOUNDTOUCH]): - if entity.entity_id == DEVICE_1_ENTITY_ID: - continue - hass.data[DATA_SOUNDTOUCH].remove(entity) - await entity.async_remove() - - # no slaves, set zone must not be called - await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_EVERYWHERE, - {"master": DEVICE_1_ENTITY_ID}, - True, - ) - assert device1_requests_mock_set_zone.call_count == 1 - async def test_create_zone( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_set_zone, @@ -567,8 +567,8 @@ async def test_create_zone( async def test_remove_zone_slave( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_remove_zone_slave, @@ -609,8 +609,8 @@ async def test_remove_zone_slave( async def test_add_zone_slave( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_add_zone_slave, @@ -651,14 +651,21 @@ async def test_add_zone_slave( async def test_zone_attributes( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, ): """Test zone attributes.""" await setup_soundtouch(hass, device1_config, device2_config) + # Fast-forward time to allow all entities to be set up and updated again + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] assert ( diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index 7f6f6970c4d..0344c09631b 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -21,13 +21,13 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: @@ -42,7 +42,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -54,7 +54,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SERVER_ID: "1", @@ -67,7 +67,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: # test setting server name to "*Auto Detect" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -79,7 +79,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "*Auto Detect", CONF_SERVER_ID: None, @@ -89,7 +89,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: # test setting the option to update periodically result2 = await hass.config_entries.options.async_init(entry.entry_id) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -114,5 +114,5 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index d00ab53645d..ba8707ed43c 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -32,7 +32,7 @@ async def test_user(hass, spider): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -45,7 +45,7 @@ async def test_user(hass, spider): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -72,7 +72,7 @@ async def test_import(hass, spider): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -91,7 +91,7 @@ async def test_abort_if_already_setup(hass, spider): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" # Should fail, config exist (flow) @@ -99,5 +99,5 @@ async def test_abort_if_already_setup(hass, spider): DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 3b1e4851ff1..8f3b5799374 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -2,14 +2,20 @@ from http import HTTPStatus from unittest.mock import patch +import pytest from spotipy import SpotifyException -from homeassistant import data_entry_flow, setup +from homeassistant import data_entry_flow from homeassistant.components import zeroconf +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.spotify.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -24,20 +30,33 @@ BLANK_ZEROCONF_INFO = zeroconf.ZeroconfServiceInfo( ) +@pytest.fixture +async def component_setup(hass: HomeAssistant) -> None: + """Fixture for setting up the integration.""" + result = await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + await async_import_client_credential( + hass, DOMAIN, ClientCredential("client", "secret"), "cred" + ) + + assert result + + async def test_abort_if_no_configuration(hass): """Check flow aborts when no configuration is present.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -49,23 +68,18 @@ async def test_zeroconf_abort_if_existing_entry(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" async def test_full_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Check a full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -79,7 +93,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://accounts.spotify.com/authorize" "?response_type=code&client_id=client" @@ -114,7 +128,7 @@ async def test_full_flow( } result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == DOMAIN + assert result["data"]["auth_implementation"] == "cred" result["data"]["token"].pop("expires_at") assert result["data"]["name"] == "frenck" assert result["data"]["token"] == { @@ -126,18 +140,13 @@ async def test_full_flow( async def test_abort_if_spotify_error( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Check Spotify errors causes flow to abort.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -169,28 +178,23 @@ async def test_abort_if_spotify_error( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "connection_error" async def test_reauthentication( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Test Spotify reauthentication.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - old_entry = MockConfigEntry( domain=DOMAIN, unique_id=123, version=1, - data={"id": "frenck", "auth_implementation": DOMAIN}, + data={"id": "frenck", "auth_implementation": "cred"}, ) old_entry.add_to_hass(hass) @@ -236,7 +240,7 @@ async def test_reauthentication( spotify_mock.return_value.current_user.return_value = {"id": "frenck"} result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == DOMAIN + assert result["data"]["auth_implementation"] == "cred" result["data"]["token"].pop("expires_at") assert result["data"]["token"] == { "refresh_token": "mock-refresh-token", @@ -247,23 +251,18 @@ async def test_reauthentication( async def test_reauth_account_mismatch( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Test Spotify reauthentication with different account.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - old_entry = MockConfigEntry( domain=DOMAIN, unique_id=123, version=1, - data={"id": "frenck", "auth_implementation": DOMAIN}, + data={"id": "frenck", "auth_implementation": "cred"}, ) old_entry.add_to_hass(hass) @@ -305,7 +304,7 @@ async def test_reauth_account_mismatch( spotify_mock.return_value.current_user.return_value = {"id": "fake_id"} result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_account_mismatch" @@ -315,5 +314,5 @@ async def test_abort_if_no_reauth_entry(hass): DOMAIN, context={"source": "reauth_confirm"} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "reauth_account_mismatch" diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 47957ead98e..7c3571f8f19 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -8,11 +8,7 @@ from sqlalchemy.exc import SQLAlchemyError from homeassistant import config_entries from homeassistant.components.sql.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( ENTRY_CONFIG, @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -43,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "db_url": "sqlite://", @@ -70,7 +66,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "db_url": "sqlite://", @@ -102,7 +98,7 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -112,7 +108,7 @@ async def test_flow_fails_db_url(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -133,7 +129,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER result5 = await hass.config_entries.flow.async_configure( @@ -141,7 +137,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: user_input=ENTRY_CONFIG_INVALID_QUERY, ) - assert result5["type"] == RESULT_TYPE_FORM + assert result5["type"] == FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -151,7 +147,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: user_input=ENTRY_CONFIG_NO_RESULTS, ) - assert result5["type"] == RESULT_TYPE_FORM + assert result5["type"] == FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -161,7 +157,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: user_input=ENTRY_CONFIG, ) - assert result5["type"] == RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == FlowResultType.CREATE_ENTRY assert result5["title"] == "Get Value" assert result5["options"] == { "db_url": "sqlite://", @@ -198,7 +194,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -211,7 +207,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "db_url": "sqlite://", @@ -243,7 +239,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -262,7 +258,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value Title", "db_url": "sqlite://", @@ -347,7 +343,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_OPT, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == { "query": "query_invalid", } @@ -362,7 +358,7 @@ async def test_options_flow_fails_invalid_query( }, ) - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["data"] == { "name": "Get Value", "value_template": None, diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 22181d73fd3..a36abcc77aa 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.squeezebox.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -50,7 +46,7 @@ async def test_user_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" assert CONF_HOST in result["data_schema"].schema for key in result["data_schema"].schema: @@ -62,7 +58,7 @@ async def test_user_form(hass): result["flow_id"], {CONF_HOST: HOST, CONF_PORT: PORT, CONF_USERNAME: "", CONF_PASSWORD: ""}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == { CONF_HOST: HOST, @@ -84,14 +80,14 @@ async def test_user_form_timeout(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} # simulate manual input of host result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: HOST2} ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "edit" assert CONF_HOST in result2["data_schema"].schema for key in result2["data_schema"].schema: @@ -113,7 +109,7 @@ async def test_user_form_duplicate(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} @@ -138,7 +134,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -162,7 +158,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -177,7 +173,7 @@ async def test_discovery(hass): context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" @@ -189,7 +185,7 @@ async def test_discovery_no_uuid(hass): context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" @@ -207,7 +203,7 @@ async def test_dhcp_discovery(hass): hostname="any", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" @@ -226,7 +222,7 @@ async def test_dhcp_discovery_no_server_found(hass): hostname="any", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -245,4 +241,4 @@ async def test_dhcp_discovery_existing_player(hass): hostname="any", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 54f8629a980..6eac5298ae4 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -13,7 +13,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -104,7 +104,7 @@ async def test_config(hass): context={"source": config_entries.SOURCE_IMPORT}, data=ENTRY_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -116,5 +116,5 @@ async def test_integration_already_configured(hass): result = await hass.config_entries.flow.async_init( SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/steam_online/__init__.py b/tests/components/steam_online/__init__.py index 4c8c398502f..d41554e4d04 100644 --- a/tests/components/steam_online/__init__.py +++ b/tests/components/steam_online/__init__.py @@ -30,15 +30,6 @@ CONF_OPTIONS_2 = { } } -CONF_IMPORT_OPTIONS = { - CONF_ACCOUNTS: { - ACCOUNT_1: ACCOUNT_NAME_1, - ACCOUNT_2: ACCOUNT_NAME_2, - } -} - -CONF_IMPORT_DATA = {CONF_API_KEY: API_KEY, CONF_ACCOUNTS: [ACCOUNT_1, ACCOUNT_2]} - def create_entry(hass: HomeAssistant) -> MockConfigEntry: """Add config entry in Home Assistant.""" diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index c4504bf1641..1844611530d 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -5,7 +5,7 @@ import steam from homeassistant import data_entry_flow from homeassistant.components.steam_online.const import CONF_ACCOUNTS, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -15,8 +15,6 @@ from . import ( ACCOUNT_2, ACCOUNT_NAME_1, CONF_DATA, - CONF_IMPORT_DATA, - CONF_IMPORT_OPTIONS, CONF_OPTIONS, CONF_OPTIONS_2, create_entry, @@ -40,7 +38,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == ACCOUNT_NAME_1 assert result["data"] == CONF_DATA assert result["options"] == CONF_OPTIONS @@ -54,7 +52,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -66,7 +64,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -77,7 +75,7 @@ async def test_flow_user_invalid_account(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_account" @@ -89,7 +87,7 @@ async def test_flow_user_unknown(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -102,7 +100,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -119,52 +117,24 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" new_conf = CONF_DATA | {CONF_API_KEY: "1234567890"} result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf -async def test_flow_import(hass: HomeAssistant) -> None: - """Test import step.""" - with patch_interface(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == ACCOUNT_NAME_1 - assert result["data"] == CONF_DATA - assert result["options"] == CONF_IMPORT_OPTIONS - assert result["result"].unique_id == ACCOUNT_1 - - -async def test_flow_import_already_configured(hass: HomeAssistant) -> None: - """Test import step already configured.""" - create_entry(hass) - with patch_interface(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_options_flow(hass: HomeAssistant) -> None: """Test updating options.""" entry = create_entry(hass) @@ -173,7 +143,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -182,7 +152,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS_2 assert len(er.async_get(hass).entities) == 2 @@ -195,7 +165,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -203,7 +173,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: user_input={CONF_ACCOUNTS: []}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ACCOUNTS: {}} assert len(er.async_get(hass).entities) == 0 @@ -215,7 +185,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: servicemock.side_effect = steam.api.HTTPTimeoutError result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -224,7 +194,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS @@ -234,7 +204,7 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: with patch_interface_private(): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -243,5 +213,5 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index ed887bb6049..0472b847e4e 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components import dhcp from homeassistant.components.steamist.const import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_DATA, @@ -46,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(no_device=True), patch( @@ -63,7 +59,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "127.0.0.1" assert result2["data"] == { "host": "127.0.0.1", @@ -76,7 +72,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(), patch( @@ -93,7 +89,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == DEVICE_NAME assert result2["data"] == DEFAULT_ENTRY_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -116,7 +112,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -137,7 +133,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -148,13 +144,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -162,7 +158,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -183,7 +179,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == DEVICE_NAME assert result3["data"] == DEFAULT_ENTRY_DATA mock_setup.assert_called_once() @@ -193,7 +189,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -201,7 +197,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -215,7 +211,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DISCOVERY_30303, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -225,7 +221,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -239,7 +235,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -254,7 +250,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), patch( @@ -265,7 +261,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -282,7 +278,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), patch( @@ -293,7 +289,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -312,7 +308,7 @@ async def test_discovered_by_dhcp_discovery_fails(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -331,7 +327,7 @@ async def test_discovered_by_dhcp_discovery_finds_non_steamist_device( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_steamist_device" @@ -359,7 +355,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == FORMATTED_MAC_ADDRESS @@ -393,7 +389,7 @@ async def test_discovered_by_dhcp_or_discovery_existing_unique_id_does_not_reloa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert not mock_setup.called assert not mock_setup_entry.called diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py index 6b5dd6fd4ce..50cd56341d6 100644 --- a/tests/components/stookalert/test_config_flow.py +++ b/tests/components/stookalert/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import patch from homeassistant.components.stookalert.const import CONF_PROVINCE, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Overijssel" assert result2.get("data") == { CONF_PROVINCE: "Overijssel", @@ -61,5 +57,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 91b4106c1f4..ff1a66a1215 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -12,10 +12,10 @@ so that it can inspect the output. from __future__ import annotations import asyncio +from collections.abc import Generator from http import HTTPStatus import logging import threading -from typing import Generator from unittest.mock import Mock, patch from aiohttp import web diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index d7595b47679..a070f609129 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -71,7 +71,7 @@ async def test_record_stream(hass, filename, h264_video): assert os.path.exists(filename) -async def test_record_lookback(hass, h264_video): +async def test_record_lookback(hass, filename, h264_video): """Exercise record with loopback.""" stream = create_stream(hass, h264_video, {}) @@ -81,7 +81,7 @@ async def test_record_lookback(hass, h264_video): await stream.start() with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path", lookback=4) + await stream.async_record(filename, lookback=4) # This test does not need recorder cleanup since it is not fully exercised @@ -245,10 +245,10 @@ async def test_record_stream_audio( await hass.async_block_till_done() -async def test_recorder_log(hass, caplog): +async def test_recorder_log(hass, filename, caplog): """Test starting a stream to record logs the url without username and password.""" stream = create_stream(hass, "https://abcd:efgh@foo.bar", {}) with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") + await stream.async_record(filename) assert "https://abcd:efgh@foo.bar" not in caplog.text assert "https://****:****@foo.bar" in caplog.text diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 65a01051606..94d77e7657e 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -71,6 +71,12 @@ SEGMENTS_PER_PACKET = PACKET_DURATION / SEGMENT_DURATION TIMEOUT = 15 +@pytest.fixture +def filename(tmpdir): + """Use this filename for the tests.""" + return f"{tmpdir}/test.mp4" + + @pytest.fixture(autouse=True) def mock_stream_settings(hass): """Set the stream settings data in hass before each test.""" @@ -897,7 +903,7 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream): } -async def test_get_image(hass): +async def test_get_image(hass, filename): """Test that the has_keyframe metadata matches the media.""" await async_setup_component(hass, "stream", {"stream": {}}) @@ -911,7 +917,7 @@ async def test_get_image(hass): stream = create_stream(hass, source, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): - make_recording = hass.async_create_task(stream.async_record("/example/path")) + make_recording = hass.async_create_task(stream.async_record(filename)) await make_recording assert stream._keyframe_converter._image is None diff --git a/tests/components/sun/test_config_flow.py b/tests/components/sun/test_config_flow.py index 1712e8f6fc9..7d20a57ba27 100644 --- a/tests/components/sun/test_config_flow.py +++ b/tests/components/sun/test_config_flow.py @@ -6,11 +6,7 @@ import pytest from homeassistant.components.sun.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +17,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -34,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} @@ -55,7 +51,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -69,7 +65,7 @@ async def test_import_flow( data={}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index e504a807f08..5f6aa6311e0 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -6,11 +6,7 @@ from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError from homeassistant import config_entries from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -26,7 +22,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -42,7 +38,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Sure Petcare" assert result2["data"] == { "username": "test-username", @@ -70,7 +66,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -92,7 +88,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -114,7 +110,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -145,7 +141,7 @@ async def test_flow_entry_already_exists( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index d80f7e24bb1..8664e3e0379 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -33,7 +33,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -45,7 +45,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { @@ -88,7 +88,7 @@ async def test_config_flow_registered_entity( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -100,7 +100,7 @@ async def test_config_flow_registered_entity( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 376406ac50c..b4b2e56b39c 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -1,7 +1,11 @@ """Tests for the switchbot integration.""" from unittest.mock import patch -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -11,31 +15,37 @@ DOMAIN = "switchbot" ENTRY_CONFIG = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:99:99:99", + CONF_ADDRESS: "e7:89:43:99:99:99", } USER_INPUT = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:99:99:99", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_CURTAIN = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:90:90:90", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", +} + +USER_INPUT_SENSOR = { + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_UNSUPPORTED_DEVICE = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "test", + CONF_ADDRESS: "test", } USER_INPUT_INVALID = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "invalid-mac", + CONF_ADDRESS: "invalid-mac", } @@ -62,3 +72,67 @@ async def init_integration( await hass.async_block_till_done() return entry + + +WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), +) +WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoCurtain", + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={89: b"\xc1\xc7'}U\xab"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Y\x00\x11\x04"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoCurtain", + manufacturer_data={89: b"\xc1\xc7'}U\xab"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Y\x00\x11\x04"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoCurtain"), +) + +WOSENSORTH_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoSensorTH", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={2409: b"\xda,\x1e\xb1\x86Au\x03\x00\x96\xac"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00d\x00\x96\xac"}, + rssi=-60, + source="local", + advertisement=AdvertisementData( + manufacturer_data={2409: b"\xda,\x1e\xb1\x86Au\x03\x00\x96\xac"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00d\x00\x96\xac"}, + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoSensorTH"), +) + +NOT_SWITCHBOT_INFO = BluetoothServiceInfoBleak( + name="unknown", + service_uuids=[], + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={}, + service_data={}, + rssi=-60, + source="local", + advertisement=AdvertisementData( + manufacturer_data={}, + service_data={}, + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "unknown"), +) diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 550aeb08082..3df082c4361 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -1,124 +1,8 @@ """Define fixtures available for all tests.""" -import sys -from unittest.mock import MagicMock, patch -from pytest import fixture +import pytest -class MocGetSwitchbotDevices: - """Scan for all Switchbot devices and return by type.""" - - def __init__(self, interface=None) -> None: - """Get switchbot devices class constructor.""" - self._interface = interface - self._all_services_data = { - "e78943999999": { - "mac_address": "e7:89:43:99:99:99", - "isEncrypted": False, - "model": "H", - "data": { - "switchMode": "true", - "isOn": "true", - "battery": 91, - "rssi": -71, - }, - "modelName": "WoHand", - }, - "e78943909090": { - "mac_address": "e7:89:43:90:90:90", - "isEncrypted": False, - "model": "c", - "data": { - "calibration": True, - "battery": 74, - "inMotion": False, - "position": 100, - "lightLevel": 2, - "deviceChain": 1, - "rssi": -73, - }, - "modelName": "WoCurtain", - }, - "ffffff19ffff": { - "mac_address": "ff:ff:ff:19:ff:ff", - "isEncrypted": False, - "model": "m", - "rawAdvData": "000d6d00", - }, - "c0ceb0d426be": { - "mac_address": "c0:ce:b0:d4:26:be", - "isEncrypted": False, - "data": { - "temp": {"c": 21.6, "f": 70.88}, - "fahrenheit": False, - "humidity": 73, - "battery": 100, - "rssi": -58, - }, - "model": "T", - "modelName": "WoSensorTH", - }, - } - self._curtain_all_services_data = { - "mac_address": "e7:89:43:90:90:90", - "isEncrypted": False, - "model": "c", - "data": { - "calibration": True, - "battery": 74, - "position": 100, - "lightLevel": 2, - "rssi": -73, - }, - "modelName": "WoCurtain", - } - self._unsupported_device = { - "mac_address": "test", - "isEncrypted": False, - "model": "HoN", - "data": { - "switchMode": "true", - "isOn": "true", - "battery": 91, - "rssi": -71, - }, - "modelName": "WoOther", - } - - async def discover(self, retry=0, scan_timeout=0): - """Mock discover.""" - return self._all_services_data - - async def get_device_data(self, mac=None): - """Return data for specific device.""" - if mac == "e7:89:43:99:99:99": - return self._all_services_data - if mac == "test": - return self._unsupported_device - if mac == "e7:89:43:90:90:90": - return self._curtain_all_services_data - - return None - - -class MocNotConnectedError(Exception): - """Mock exception.""" - - -module = type(sys)("switchbot") -module.GetSwitchbotDevices = MocGetSwitchbotDevices -module.NotConnectedError = MocNotConnectedError -sys.modules["switchbot"] = module - - -@fixture -def switchbot_config_flow(hass): - """Mock the bluepy api for easier config flow testing.""" - with patch.object(MocGetSwitchbotDevices, "discover", return_value=True), patch( - "homeassistant.components.switchbot.config_flow.GetSwitchbotDevices" - ) as mock_switchbot: - instance = mock_switchbot.return_value - - instance.discover = MagicMock(return_value=True) - - yield mock_switchbot +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 59871681dfe..0ae3430eeb1 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -1,32 +1,102 @@ """Test the switchbot config flow.""" -from homeassistant.components.switchbot.config_flow import NotConnectedError -from homeassistant.components.switchbot.const import ( - CONF_RETRY_COUNT, - CONF_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT, - CONF_TIME_BETWEEN_UPDATE_COMMAND, -) -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, +from unittest.mock import patch + +from homeassistant.components.switchbot.const import CONF_RETRY_COUNT +from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + NOT_SWITCHBOT_INFO, + USER_INPUT, + USER_INPUT_CURTAIN, + USER_INPUT_SENSOR, + WOCURTAIN_SERVICE_INFO, + WOHAND_SERVICE_INFO, + WOSENSORTH_SERVICE_INFO, + init_integration, + patch_async_setup_entry, ) -from . import USER_INPUT, USER_INPUT_CURTAIN, init_integration, patch_async_setup_entry +from tests.common import MockConfigEntry DOMAIN = "switchbot" -async def test_user_form_valid_mac(hass): +async def test_bluetooth_discovery(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOHAND_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-name" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_discovery_already_setup(hass): + """Test discovery via bluetooth with a valid device when already setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + unique_id="aabbccddeeff", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOHAND_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_not_switchbot(hass): + """Test discovery via bluetooth not switchbot.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=NOT_SWITCHBOT_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_user_setup_wohand(hass): """Test the user initiated form with password and valid mac.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_FORM + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOHAND_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -37,10 +107,10 @@ async def test_user_form_valid_mac(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { - CONF_MAC: "e7:89:43:99:99:99", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", @@ -48,12 +118,42 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 - # test curtain device creation. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} +async def test_user_setup_wohand_already_configured(hass): + """Test the user initiated form with password and valid mac.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + unique_id="aabbccddeeff", ) - assert result["type"] == RESULT_TYPE_FORM + entry.add_to_hass(hass) + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOHAND_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_setup_wocurtain(hass): + """Test the user initiated form with password and valid mac.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -64,10 +164,10 @@ async def test_user_form_valid_mac(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { - CONF_MAC: "e7:89:43:90:90:90", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "curtain", @@ -75,65 +175,129 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 - # tests abort if no unconfigured devices are found. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_ABORT +async def test_user_setup_wosensor(hass): + """Test the user initiated form with password and valid mac.""" + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOSENSORTH_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT_SENSOR, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-name" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "hygrometer", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_no_devices(hass): + """Test the user initiated form with password and valid mac.""" + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_unconfigured_devices" -async def test_user_form_exception(hass, switchbot_config_flow): - """Test we handle exception on user form.""" - - switchbot_config_flow.side_effect = NotConnectedError - +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOCURTAIN_SERVICE_INFO, ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM - switchbot_config_flow.side_effect = Exception + with patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT, + ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "test-name" + assert result2["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "curtain", + } - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "unknown" + assert len(mock_setup_entry.mock_calls) == 1 + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) async def test_options_flow(hass): """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + options={ + CONF_RETRY_COUNT: 10, + }, + unique_id="aabbccddeeff", + ) + entry.add_to_hass(hass) + with patch_async_setup_entry() as mock_setup_entry: entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_TIME_BETWEEN_UPDATE_COMMAND: 60, CONF_RETRY_COUNT: 3, - CONF_RETRY_TIMEOUT: 5, - CONF_SCAN_TIMEOUT: 5, }, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 60 + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 3 - assert result["data"][CONF_RETRY_TIMEOUT] == 5 - assert result["data"][CONF_SCAN_TIMEOUT] == 5 - assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 # Test changing of entry options. @@ -141,25 +305,21 @@ async def test_options_flow(hass): entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_TIME_BETWEEN_UPDATE_COMMAND: 66, CONF_RETRY_COUNT: 6, - CONF_RETRY_TIMEOUT: 6, - CONF_SCAN_TIMEOUT: 6, }, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 66 + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 6 - assert result["data"][CONF_RETRY_TIMEOUT] == 6 - assert result["data"][CONF_SCAN_TIMEOUT] == 6 assert len(mock_setup_entry.mock_calls) == 1 + + assert entry.options[CONF_RETRY_COUNT] == 6 diff --git a/tests/components/switcher_kis/test_config_flow.py b/tests/components/switcher_kis/test_config_flow.py index 2029e4a8ef3..f5ad68b6033 100644 --- a/tests/components/switcher_kis/test_config_flow.py +++ b/tests/components/switcher_kis/test_config_flow.py @@ -5,11 +5,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.switcher_kis.const import DATA_DISCOVERY, DOMAIN -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE @@ -25,7 +21,7 @@ async def test_import(hass): DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Switcher" assert result["data"] == {} @@ -53,7 +49,7 @@ async def test_user_setup(hass, mock_bridge): assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 2 - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -62,7 +58,7 @@ async def test_user_setup(hass, mock_bridge): ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Switcher" assert result2["result"].data == {} @@ -78,13 +74,13 @@ async def test_user_setup_abort_no_devices_found(hass, mock_bridge): assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 0 - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -104,5 +100,5 @@ async def test_single_instance(hass, source): DOMAIN, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py index 80656c75990..6318f4da92e 100644 --- a/tests/components/syncthing/test_config_flow.py +++ b/tests/components/syncthing/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_setup_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "user" @@ -52,7 +52,7 @@ async def test_flow_successful(hass): CONF_VERIFY_SSL: VERIFY_SSL, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:8384" assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_URL] == URL @@ -74,7 +74,7 @@ async def test_flow_already_configured(hass): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -88,7 +88,7 @@ async def test_flow_invalid_auth(hass): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["token"] == "invalid_auth" @@ -102,5 +102,5 @@ async def test_flow_cannot_connect(hass): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 7a91ff7a735..40ed12d0d0b 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -43,7 +43,7 @@ async def test_show_setup_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -65,7 +65,7 @@ async def test_already_configured_by_url(hass, aioclient_mock): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_NAME] == FIXTURE_USER_INPUT[CONF_NAME] assert result["result"].unique_id == udn @@ -80,7 +80,7 @@ async def test_syncthru_not_supported(hass): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "syncthru_not_supported"} @@ -96,7 +96,7 @@ async def test_unknown_state(hass): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "unknown_state"} @@ -115,7 +115,7 @@ async def test_success(hass, aioclient_mock): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -144,7 +144,7 @@ async def test_ssdp(hass, aioclient_mock): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" assert CONF_URL in result["data_schema"].schema for k in result["data_schema"].schema: diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 8cec30abefa..015c3a2ab16 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -120,7 +120,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -136,7 +136,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -163,7 +163,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -185,7 +185,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2sa" # Failed the first time because was too slow to enter the code @@ -195,7 +195,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_OTP_CODE: "000000"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2sa" assert result["errors"] == {CONF_OTP_CODE: "otp_failed"} @@ -206,7 +206,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): result["flow_id"], {CONF_OTP_CODE: "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -226,7 +226,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -242,7 +242,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -288,7 +288,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -298,7 +298,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -323,7 +323,7 @@ async def test_reconfig_user(hass: HomeAssistant, service: MagicMock): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -338,7 +338,7 @@ async def test_login_failed(hass: HomeAssistant, service: MagicMock): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_USERNAME: "invalid_auth"} @@ -354,7 +354,7 @@ async def test_connection_failed(hass: HomeAssistant, service: MagicMock): data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -368,7 +368,7 @@ async def test_unknown_failed(hass: HomeAssistant, service: MagicMock): data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -379,7 +379,7 @@ async def test_missing_data_after_login(hass: HomeAssistant, service_failed: Mag context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "missing_data"} @@ -399,7 +399,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -407,7 +407,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == "192.168.1.5" assert result["data"][CONF_HOST] == "192.168.1.5" @@ -450,7 +450,7 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -482,7 +482,7 @@ async def test_skip_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -514,7 +514,7 @@ async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -535,7 +535,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): assert config_entry.options == {} result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # Scan interval @@ -544,7 +544,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL assert config_entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT assert config_entry.options[CONF_SNAPSHOT_QUALITY] == DEFAULT_SNAPSHOT_QUALITY @@ -555,7 +555,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2, CONF_TIMEOUT: 30, CONF_SNAPSHOT_QUALITY: 0}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 assert config_entry.options[CONF_TIMEOUT] == 30 assert config_entry.options[CONF_SNAPSHOT_QUALITY] == 0 diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index db373f41656..da2916b8c81 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -52,7 +52,7 @@ async def test_reauth_triggered(hass: HomeAssistant): side_effect=SynologyDSMLoginInvalidException(USERNAME), ), patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth", - return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + return_value={"type": data_entry_flow.FlowResultType.FORM}, ) as mock_async_step_reauth: entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 515146bc16c..45131353550 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -102,7 +102,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -112,7 +112,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch( @@ -129,7 +129,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "test-bridge" assert result2["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch( @@ -153,7 +153,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -164,7 +164,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -178,7 +178,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -189,7 +189,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -203,7 +203,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -214,7 +214,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -228,7 +228,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -239,7 +239,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -253,7 +253,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -264,7 +264,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -278,7 +278,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -289,7 +289,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -303,7 +303,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "invalid_auth"} @@ -314,7 +314,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch( @@ -326,7 +326,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -337,7 +337,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -351,7 +351,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -367,7 +367,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -384,7 +384,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -399,7 +399,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -416,7 +416,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == FIXTURE_ZEROCONF_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -431,7 +431,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] with patch( @@ -443,7 +443,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -457,5 +457,5 @@ async def test_zeroconf_bad_zeroconf_info(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF_BAD, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 121c29d2eed..6304e0ea7cf 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -1,10 +1,11 @@ """Test system log component.""" -import asyncio -import logging -import queue -from unittest.mock import MagicMock, patch +from __future__ import annotations -import pytest +import asyncio +from collections.abc import Awaitable +import logging +from typing import Any +from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log @@ -16,28 +17,6 @@ _LOGGER = logging.getLogger("test_logger") BASIC_CONFIG = {"system_log": {"max_entries": 2}} -@pytest.fixture -def simple_queue(): - """Fixture that get the queue.""" - simple_queue_fixed = queue.SimpleQueue() - with patch( - "homeassistant.components.system_log.queue.SimpleQueue", - return_value=simple_queue_fixed, - ): - yield simple_queue_fixed - - -async def _async_block_until_queue_empty(hass, sq): - # Unfortunately we are stuck with polling - await hass.async_block_till_done() - while not sq.empty(): - await asyncio.sleep(0.01) - hass.data[system_log.DOMAIN].acquire() - hass.data[system_log.DOMAIN].release() - await hass.async_block_till_done() - await hass.async_block_till_done() - - async def get_error_log(hass_ws_client): """Fetch all entries from system_log via the API.""" client = await hass_ws_client() @@ -81,57 +60,97 @@ def assert_log(log, exception, message, level): assert "timestamp" in log +class WatchLogErrorHandler(system_log.LogErrorHandler): + """WatchLogErrorHandler that watches for a message.""" + + instances: list[WatchLogErrorHandler] = [] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize HASSQueueListener.""" + super().__init__(*args, **kwargs) + self.watch_message: str | None = None + self.watch_event: asyncio.Event | None = asyncio.Event() + WatchLogErrorHandler.instances.append(self) + + def add_watcher(self, match: str) -> Awaitable: + """Add a watcher.""" + self.watch_event = asyncio.Event() + self.watch_message = match + return self.watch_event.wait() + + def handle(self, record: logging.LogRecord) -> None: + """Handle a logging record.""" + super().handle(record) + if record.message in self.watch_message: + self.watch_event.set() + + def get_frame(name): """Get log stack frame.""" return (name, 5, None, None) -async def test_normal_logs(hass, simple_queue, hass_ws_client): +async def async_setup_system_log(hass, config) -> WatchLogErrorHandler: + """Set up the system_log component.""" + WatchLogErrorHandler.instances = [] + with patch( + "homeassistant.components.system_log.LogErrorHandler", WatchLogErrorHandler + ): + await async_setup_component(hass, system_log.DOMAIN, config) + await hass.async_block_till_done() + + assert len(WatchLogErrorHandler.instances) == 1 + return WatchLogErrorHandler.instances.pop() + + +async def test_normal_logs(hass, hass_ws_client): """Test that debug and info are not logged.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) - + await hass.async_block_till_done() _LOGGER.debug("debug") _LOGGER.info("info") - await _async_block_until_queue_empty(hass, simple_queue) # Assert done by get_error_log logs = await get_error_log(hass_ws_client) assert len([msg for msg in logs if msg["level"] in ("DEBUG", "INFO")]) == 0 -async def test_exception(hass, simple_queue, hass_ws_client): +async def test_exception(hass, hass_ws_client): """Test that exceptions are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _generate_and_log_exception("exception message", "log message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "ERROR") assert log is not None assert_log(log, "exception message", "log message", "ERROR") -async def test_warning(hass, simple_queue, hass_ws_client): +async def test_warning(hass, hass_ws_client): """Test that warning are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() _LOGGER.warning("warning message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "WARNING") assert_log(log, "", "warning message", "WARNING") -async def test_error(hass, simple_queue, hass_ws_client): +async def test_error(hass, hass_ws_client): """Test that errors are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "ERROR") assert_log(log, "", "error message", "ERROR") -async def test_config_not_fire_event(hass, simple_queue): +async def test_config_not_fire_event(hass): """Test that errors are not posted as events with default config.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() events = [] @callback @@ -141,44 +160,49 @@ async def test_config_not_fire_event(hass, simple_queue): hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) - _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) + await hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 0 -async def test_error_posted_as_event(hass, simple_queue): +async def test_error_posted_as_event(hass): """Test that error are posted as events.""" - await async_setup_component( - hass, system_log.DOMAIN, {"system_log": {"max_entries": 2, "fire_event": True}} + watcher = await async_setup_system_log( + hass, {"system_log": {"max_entries": 2, "fire_event": True}} ) + wait_empty = watcher.add_watcher("error message") + events = async_capture_events(hass, system_log.EVENT_SYSTEM_LOG) _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) + await wait_empty + await hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 1 assert_log(events[0].data, "", "error message", "ERROR") -async def test_critical(hass, simple_queue, hass_ws_client): +async def test_critical(hass, hass_ws_client): """Test that critical are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.critical("critical message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "CRITICAL") assert_log(log, "", "critical message", "CRITICAL") -async def test_remove_older_logs(hass, simple_queue, hass_ws_client): +async def test_remove_older_logs(hass, hass_ws_client): """Test that older logs are rotated out.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() _LOGGER.error("error message 1") _LOGGER.error("error message 2") _LOGGER.error("error message 3") - await _async_block_until_queue_empty(hass, simple_queue) - + await hass.async_block_till_done() log = await get_error_log(hass_ws_client) assert_log(log[0], "", "error message 3", "ERROR") assert_log(log[1], "", "error message 2", "ERROR") @@ -189,14 +213,14 @@ def log_msg(nr=2): _LOGGER.error("error message %s", nr) -async def test_dedupe_logs(hass, simple_queue, hass_ws_client): +async def test_dedupe_logs(hass, hass_ws_client): """Test that duplicate log entries are dedupe.""" - await async_setup_component(hass, system_log.DOMAIN, {}) + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() _LOGGER.error("error message 1") log_msg() log_msg("2-2") _LOGGER.error("error message 3") - await _async_block_until_queue_empty(hass, simple_queue) log = await get_error_log(hass_ws_client) assert_log(log[0], "", "error message 3", "ERROR") @@ -204,8 +228,6 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): assert_log(log[1], "", ["error message 2", "error message 2-2"], "ERROR") log_msg() - await _async_block_until_queue_empty(hass, simple_queue) - log = await get_error_log(hass_ws_client) assert_log(log[0], "", ["error message 2", "error message 2-2"], "ERROR") assert log[0]["timestamp"] > log[0]["first_occurred"] @@ -214,7 +236,6 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): log_msg("2-4") log_msg("2-5") log_msg("2-6") - await _async_block_until_queue_empty(hass, simple_queue) log = await get_error_log(hass_ws_client) assert_log( @@ -231,15 +252,14 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): ) -async def test_clear_logs(hass, simple_queue, hass_ws_client): +async def test_clear_logs(hass, hass_ws_client): """Test that the log can be cleared via a service call.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) await hass.services.async_call(system_log.DOMAIN, system_log.SERVICE_CLEAR, {}) - await _async_block_until_queue_empty(hass, simple_queue) - + await hass.async_block_till_done() # Assert done by get_error_log await get_error_log(hass_ws_client) @@ -247,6 +267,8 @@ async def test_clear_logs(hass, simple_queue, hass_ws_client): async def test_write_log(hass): """Test that error propagates to logger.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + logger = MagicMock() with patch("logging.getLogger", return_value=logger) as mock_logging: await hass.services.async_call( @@ -260,6 +282,8 @@ async def test_write_log(hass): async def test_write_choose_logger(hass): """Test that correct logger is chosen.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + with patch("logging.getLogger") as mock_logging: await hass.services.async_call( system_log.DOMAIN, @@ -273,6 +297,8 @@ async def test_write_choose_logger(hass): async def test_write_choose_level(hass): """Test that correct logger is chosen.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + logger = MagicMock() with patch("logging.getLogger", return_value=logger): await hass.services.async_call( @@ -284,17 +310,17 @@ async def test_write_choose_level(hass): assert logger.method_calls[0] == ("debug", ("test_message",)) -async def test_unknown_path(hass, simple_queue, hass_ws_client): +async def test_unknown_path(hass, hass_ws_client): """Test error logged from unknown path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() _LOGGER.findCaller = MagicMock(return_value=("unknown_path", 0, None, None)) _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["unknown_path", 0] -async def async_log_error_from_test_path(hass, path, sq): +async def async_log_error_from_test_path(hass, path, watcher): """Log error while mocking the path.""" call_path = "internal_path.py" with patch.object( @@ -310,30 +336,34 @@ async def async_log_error_from_test_path(hass, path, sq): ] ), ): + wait_empty = watcher.add_watcher("error message") _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, sq) + await wait_empty -async def test_homeassistant_path(hass, simple_queue, hass_ws_client): +async def test_homeassistant_path(hass, hass_ws_client): """Test error logged from Home Assistant path.""" - await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + with patch( "homeassistant.components.system_log.HOMEASSISTANT_PATH", new=["venv_path/homeassistant"], ): + watcher = await async_setup_system_log(hass, BASIC_CONFIG) await async_log_error_from_test_path( - hass, "venv_path/homeassistant/component/component.py", simple_queue + hass, "venv_path/homeassistant/component/component.py", watcher ) log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["component/component.py", 5] -async def test_config_path(hass, simple_queue, hass_ws_client): +async def test_config_path(hass, hass_ws_client): """Test error logged from config path.""" - await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + with patch.object(hass.config, "config_dir", new="config"): + watcher = await async_setup_system_log(hass, BASIC_CONFIG) + await async_log_error_from_test_path( - hass, "config/custom_component/test.py", simple_queue + hass, "config/custom_component/test.py", watcher ) log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["custom_component/test.py", 5] diff --git a/tests/components/tailscale/test_binary_sensor.py b/tests/components/tailscale/test_binary_sensor.py index feb34c6d8a3..864fb497134 100644 --- a/tests/components/tailscale/test_binary_sensor.py +++ b/tests/components/tailscale/test_binary_sensor.py @@ -43,7 +43,7 @@ async def test_tailscale_binary_sensors( assert state.state == STATE_OFF assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "frencks-iphone Supports Hairpinning" + == "frencks-iphone Supports hairpinning" ) assert state.attributes.get(ATTR_ICON) == "mdi:wan" assert ATTR_DEVICE_CLASS not in state.attributes diff --git a/tests/components/tailscale/test_config_flow.py b/tests/components/tailscale/test_config_flow.py index eb070cfdbb2..78e3f20a61b 100644 --- a/tests/components/tailscale/test_config_flow.py +++ b/tests/components/tailscale/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.tailscale.const import CONF_TAILNET, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -39,7 +35,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "homeassistant.github" assert result2.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -64,7 +60,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -77,7 +73,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -94,7 +90,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "homeassistant.github" assert result3.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -120,7 +116,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 @@ -144,7 +140,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -154,7 +150,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -187,7 +183,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -198,7 +194,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -213,7 +209,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("type") == FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -241,7 +237,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -252,6 +248,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/tailscale/test_sensor.py b/tests/components/tailscale/test_sensor.py index 911de0eb64a..9f55bba2c70 100644 --- a/tests/components/tailscale/test_sensor.py +++ b/tests/components/tailscale/test_sensor.py @@ -35,7 +35,7 @@ async def test_tailscale_sensors( assert entry.unique_id == "123457_last_seen" assert entry.entity_category is None assert state.state == "2021-11-15T20:37:03+00:00" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Last Seen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Last seen" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes @@ -46,7 +46,7 @@ async def test_tailscale_sensors( assert entry.unique_id == "123457_ip" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "100.11.11.112" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router IP Address" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router IP address" assert state.attributes.get(ATTR_ICON) == "mdi:ip-network" assert ATTR_DEVICE_CLASS not in state.attributes diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index cae78a447f8..600bfd98c73 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -19,11 +19,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -91,7 +87,7 @@ async def test_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -103,13 +99,13 @@ async def test_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_STATIONS_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_NAME] == "Home" assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx" assert result["data"][CONF_FUEL_TYPES] == ["e5"] @@ -139,14 +135,14 @@ async def test_user_already_configured(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +151,7 @@ async def test_exception_security(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -166,7 +162,7 @@ async def test_exception_security(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_API_KEY] == "invalid_auth" @@ -176,7 +172,7 @@ async def test_user_no_stations(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -186,7 +182,7 @@ async def test_user_no_stations(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_RADIUS] == "no_stations" @@ -202,8 +198,8 @@ async def test_import(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_NAME] == "Home" assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx" assert result["data"][CONF_FUEL_TYPES] == ["e5"] @@ -243,7 +239,7 @@ async def test_reauth(hass: HomeAssistant): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # re-auth unsuccessful @@ -254,7 +250,7 @@ async def test_reauth(hass: HomeAssistant): CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_API_KEY: "invalid_auth"} @@ -266,7 +262,7 @@ async def test_reauth(hass: HomeAssistant): CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" mock_setup_entry.assert_called() @@ -297,7 +293,7 @@ async def test_options_flow(hass: HomeAssistant): assert mock_setup_entry.called result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -306,5 +302,5 @@ async def test_options_flow(hass: HomeAssistant): CONF_STATIONS: MOCK_OPTIONS_DATA[CONF_STATIONS], }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 3413817892b..77a2787c0d5 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -1,6 +1,6 @@ """Test config flow.""" from homeassistant import config_entries -from homeassistant.components import mqtt +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_mqtt_abort_if_existing_entry(hass, mqtt_mock): async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): """Check MQTT flow aborts if discovery topic is invalid.""" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/bla", payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' @@ -42,7 +42,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): assert result["type"] == "abort" assert result["reason"] == "invalid_discovery_info" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/config", payload="", qos=0, @@ -56,7 +56,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): assert result["type"] == "abort" assert result["reason"] == "invalid_discovery_info" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/config", payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' @@ -81,7 +81,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): async def test_mqtt_setup(hass, mqtt_mock) -> None: """Test we can finish a config flow through MQTT with custom prefix.""" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/config", payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index d37e4401275..d846f0915d0 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -32,7 +32,7 @@ async def test_flow_user_single_instance_allowed(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_IMPORT_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -41,7 +41,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -52,7 +52,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -64,7 +64,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -75,7 +75,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -87,7 +87,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -98,7 +98,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -110,7 +110,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -121,7 +121,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -141,7 +141,7 @@ async def test_flow_reauth( }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -156,7 +156,7 @@ async def test_flow_reauth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == CONF_DATA assert len(mock_entry.mock_calls) == 1 @@ -182,7 +182,7 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -191,5 +191,5 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index 7417c87c229..ba233d04a78 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -62,12 +62,12 @@ async def test_abort_if_already_setup(hass): with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -76,16 +76,16 @@ async def test_full_flow_implementation(hass, mock_tellduslive): flow = init_config_flow(hass) flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert len(flow._hosts) == 2 result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"host": "localhost"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "auth_url": "https://example.com", @@ -93,7 +93,7 @@ async def test_full_flow_implementation(hass, mock_tellduslive): } result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -105,7 +105,7 @@ async def test_step_import(hass, mock_tellduslive): flow = init_config_flow(hass) result = await flow.async_step_import({CONF_HOST: DOMAIN, KEY_SCAN_INTERVAL: 0}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -116,7 +116,7 @@ async def test_step_import_add_host(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -127,7 +127,7 @@ async def test_step_import_no_config_file(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -142,7 +142,7 @@ async def test_step_import_load_json_matching_host(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "Cloud API", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -157,7 +157,7 @@ async def test_step_import_load_json(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -171,7 +171,7 @@ async def test_step_disco_no_local_api(hass, mock_tellduslive): flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert len(flow._hosts) == 1 @@ -182,7 +182,7 @@ async def test_step_auth(hass, mock_tellduslive): await flow.async_step_auth() result = await flow.async_step_auth(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API" assert result["data"]["host"] == "Cloud API" assert result["data"]["scan_interval"] == 60 @@ -199,7 +199,7 @@ async def test_wrong_auth_flow_implementation(hass, mock_tellduslive): await flow.async_step_auth() result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"]["base"] == "invalid_auth" @@ -209,7 +209,7 @@ async def test_not_pick_host_if_only_one(hass, mock_tellduslive): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -218,7 +218,7 @@ async def test_abort_if_timeout_generating_auth_url(hass, mock_tellduslive): flow = init_config_flow(hass, side_effect=asyncio.TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -228,7 +228,7 @@ async def test_abort_no_auth_url(hass, mock_tellduslive): flow._get_auth_url = Mock(return_value=False) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -237,7 +237,7 @@ async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive): flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 8f9ab39f7c0..7ae69b2d563 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -8,8 +8,10 @@ from homeassistant.const import ( ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -56,11 +58,26 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = { "entity_id": "alarm_control_panel.test", "data": {"code": "{{ this.entity_id }}"}, }, + "arm_vacation": { + "service": "alarm_control_panel.alarm_arm_vacation", + "entity_id": "alarm_control_panel.test", + "data": {"code": "{{ this.entity_id }}"}, + }, + "arm_custom_bypass": { + "service": "alarm_control_panel.alarm_arm_custom_bypass", + "entity_id": "alarm_control_panel.test", + "data": {"code": "{{ this.entity_id }}"}, + }, "disarm": { "service": "alarm_control_panel.alarm_disarm", "entity_id": "alarm_control_panel.test", "data": {"code": "{{ this.entity_id }}"}, }, + "trigger": { + "service": "alarm_control_panel.alarm_trigger", + "entity_id": "alarm_control_panel.test", + "data": {"code": "{{ this.entity_id }}"}, + }, } @@ -89,6 +106,8 @@ async def test_template_state_text(hass, start_ha): STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -128,7 +147,10 @@ async def test_optimistic_states(hass, start_ha): ("alarm_arm_away", STATE_ALARM_ARMED_AWAY), ("alarm_arm_home", STATE_ALARM_ARMED_HOME), ("alarm_arm_night", STATE_ALARM_ARMED_NIGHT), + ("alarm_arm_vacation", STATE_ALARM_ARMED_VACATION), + ("alarm_arm_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS), ("alarm_disarm", STATE_ALARM_DISARMED), + ("alarm_trigger", STATE_ALARM_TRIGGERED), ]: await hass.services.async_call( ALARM_DOMAIN, service, {"entity_id": TEMPLATE_NAME}, blocking=True @@ -250,7 +272,10 @@ async def test_name(hass, start_ha): "alarm_arm_home", "alarm_arm_away", "alarm_arm_night", + "alarm_arm_vacation", + "alarm_arm_custom_bypass", "alarm_disarm", + "alarm_trigger", ], ) async def test_actions(hass, service, start_ha, service_calls): diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index 2e286f75b61..5c4ba11b05a 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components import dhcp from homeassistant.components.tesla_wall_connector.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -31,7 +31,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Tesla Wall Connector" assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -52,7 +52,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -73,7 +73,7 @@ async def test_form_other_error( {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -128,7 +128,7 @@ async def test_dhcp_can_finish( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: "1.2.3.4"} diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index 0a10c24a2f6..3d0ff53a4a9 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.threshold.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My threshold sensor" assert result["data"] == {} assert result["options"] == { @@ -68,7 +68,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -80,7 +80,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} @@ -118,7 +118,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "hysteresis") == 0.0 @@ -132,7 +132,7 @@ async def test_options(hass: HomeAssistant) -> None: "upper": 20.0, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_id": input_sensor, "hysteresis": 0.0, diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index 7c623de4ded..9200ed3f382 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -15,7 +15,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -35,7 +35,7 @@ async def test_errors(hass, config, err, err_string): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": err_string} @@ -44,7 +44,7 @@ async def test_step_import(hass, config, setup_tile): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@host.com" assert result["data"] == { CONF_USERNAME: "user@host.com", @@ -60,13 +60,13 @@ async def test_step_reauth(hass, config, config_entry, setup_tile): assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -76,13 +76,13 @@ async def test_step_user(hass, config, setup_tile): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@host.com" assert result["data"] == { CONF_USERNAME: "user@host.com", diff --git a/tests/components/tod/test_config_flow.py b/tests/components/tod/test_config_flow.py index 35f9ef0d5bd..e215a4f1ae9 100644 --- a/tests/components/tod/test_config_flow.py +++ b/tests/components/tod/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.tod.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -35,7 +35,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My tod" assert result["data"] == {} assert result["options"] == { @@ -85,7 +85,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "after_time") == "10:00" @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: "before_time": "17:05", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "after_time": "10:00", "before_time": "17:05", diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 9991decc511..38542ad7db5 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.tolo.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( ip="127.0.0.2", macaddress="00:11:22:33:44:55", hostname="mock_hostname" @@ -37,7 +33,7 @@ async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock): data={CONF_HOST: "127.0.0.1"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {"base": "cannot_connect"} @@ -48,7 +44,7 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert "flow_id" in result @@ -59,7 +55,7 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): user_input={CONF_HOST: "127.0.0.2"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == SOURCE_USER assert result2["errors"] == {"base": "cannot_connect"} assert "flow_id" in result2 @@ -71,7 +67,7 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): user_input={CONF_HOST: "127.0.0.1"}, ) - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "TOLO Sauna" assert result3["data"][CONF_HOST] == "127.0.0.1" @@ -83,7 +79,7 @@ async def test_dhcp(hass: HomeAssistant, toloclient: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -91,7 +87,7 @@ async def test_dhcp(hass: HomeAssistant, toloclient: Mock): user_input={}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "TOLO Sauna" assert result["data"][CONF_HOST] == "127.0.0.2" assert result["result"].unique_id == "00:11:22:33:44:55" @@ -104,4 +100,4 @@ async def test_dhcp_invalid_device(hass: HomeAssistant, toloclient: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py index ca888210a46..77af05cdc7d 100644 --- a/tests/components/tomorrowio/test_config_flow.py +++ b/tests/components/tomorrowio/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -52,7 +52,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_API_KEY] == API_KEY @@ -77,7 +77,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -85,7 +85,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_NAME] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_API_KEY] == API_KEY @@ -111,7 +111,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -143,7 +143,7 @@ async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -159,7 +159,7 @@ async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "rate_limited"} @@ -175,7 +175,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -199,14 +199,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMESTEP: 1} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMESTEP] == 1 assert entry.options[CONF_TIMESTEP] == 1 @@ -256,13 +256,13 @@ async def test_import_flow_v3( data=old_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "this is a test"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_API_KEY: "this is a test", CONF_LOCATION: { diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 826df81066b..3d7a0613269 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -37,7 +37,7 @@ async def test_abort_if_no_configuration(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -51,7 +51,7 @@ async def test_full_flow_implementation( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # pylint: disable=protected-access @@ -67,7 +67,7 @@ async def test_full_flow_implementation( result["flow_id"], {"implementation": "eneco"} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result2["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result2["url"] == ( "https://api.toon.eu/authorize" "?response_type=code&client_id=client" @@ -141,7 +141,7 @@ async def test_no_agreements( with patch("toonapi.Toon.agreements", return_value=[]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "no_agreements" @@ -185,7 +185,7 @@ async def test_multiple_agreements( ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "agreement" result4 = await hass.config_entries.flow.async_configure( @@ -232,7 +232,7 @@ async def test_agreement_already_set_up( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -271,7 +271,7 @@ async def test_toon_abort( with patch("toonapi.Toon.agreements", side_effect=ToonError): result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "connection_error" @@ -285,7 +285,7 @@ async def test_import(hass, current_request_with_host): DOMAIN, context={"source": SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -333,7 +333,7 @@ async def test_import_migration( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 78b121dda77..5e5124db71c 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -38,7 +38,7 @@ async def test_user(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -65,7 +65,7 @@ async def test_user_show_locations(hass): ) # first it should show the locations form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "locations" # client should have sent four requests for init assert mock_request.call_count == 4 @@ -75,7 +75,7 @@ async def test_user_show_locations(hass): result["flow_id"], user_input={CONF_USERCODES: "bad"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "locations" # client should have sent 5th request to validate usercode assert mock_request.call_count == 5 @@ -85,7 +85,7 @@ async def test_user_show_locations(hass): result2["flow_id"], user_input={CONF_USERCODES: "7890"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # client should have sent another request to validate usercode assert mock_request.call_count == 6 @@ -106,7 +106,7 @@ async def test_abort_if_already_setup(hass): data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -122,7 +122,7 @@ async def test_login_failed(hass): data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -138,7 +138,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -152,7 +152,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -162,7 +162,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_no_locations(hass): context={"source": SOURCE_USER}, data=CONFIG_DATA_NO_USERCODES, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_locations" await hass.async_block_till_done() @@ -221,14 +221,14 @@ async def test_options_flow(hass: HomeAssistant): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={AUTO_BYPASS: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {AUTO_BYPASS: True} await hass.async_block_till_done() diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index a3792238fb2..77ab43a05b5 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components import dhcp from homeassistant.components.tplink import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( ALIAS, @@ -256,7 +256,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data={CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_NAME: ALIAS}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_single_discovery(): @@ -268,7 +268,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_single_discovery(): @@ -280,7 +280,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with _patch_discovery(no_device=True), _patch_single_discovery(no_device=True): @@ -292,7 +292,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -318,7 +318,7 @@ async def test_discovered_by_dhcp_or_discovery(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_single_discovery(), patch( @@ -358,5 +358,5 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/traccar/test_device_tracker.py b/tests/components/traccar/test_device_tracker.py index 4e2f5e0ff09..61bbe371a75 100644 --- a/tests/components/traccar/test_device_tracker.py +++ b/tests/components/traccar/test_device_tracker.py @@ -2,6 +2,8 @@ from datetime import datetime from unittest.mock import AsyncMock, patch +from pytraccar import ReportsEventeModel + from homeassistant.components.device_tracker.const import DOMAIN from homeassistant.components.traccar.device_tracker import ( PLATFORM_SCHEMA as TRACCAR_PLATFORM_SCHEMA, @@ -35,26 +37,39 @@ async def test_import_events_catch_all(hass): device = {"id": 1, "name": "abc123"} api_mock = AsyncMock() api_mock.devices = [device] - api_mock.get_events.return_value = [ - { - "deviceId": device["id"], - "type": "ignitionOn", - "serverTime": datetime.utcnow(), - "attributes": {}, - }, - { - "deviceId": device["id"], - "type": "ignitionOff", - "serverTime": datetime.utcnow(), - "attributes": {}, - }, + api_mock.get_reports_events.return_value = [ + ReportsEventeModel( + **{ + "id": 1, + "positionId": 1, + "geofenceId": 1, + "maintenanceId": 1, + "deviceId": device["id"], + "type": "ignitionOn", + "eventTime": datetime.utcnow().isoformat(), + "attributes": {}, + } + ), + ReportsEventeModel( + **{ + "id": 2, + "positionId": 2, + "geofenceId": 1, + "maintenanceId": 1, + "deviceId": device["id"], + "type": "ignitionOff", + "eventTime": datetime.utcnow().isoformat(), + "attributes": {}, + } + ), ] events_ignition_on = async_capture_events(hass, "traccar_ignition_on") events_ignition_off = async_capture_events(hass, "traccar_ignition_off") with patch( - "homeassistant.components.traccar.device_tracker.API", return_value=api_mock + "homeassistant.components.traccar.device_tracker.ApiClient", + return_value=api_mock, ): assert await async_setup_component(hass, DOMAIN, conf_dict) diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 2d0140db815..830670efc11 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -63,10 +63,10 @@ async def webhook_id_fixture(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 0ac431a7f35..e6de115c1ca 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -35,7 +35,7 @@ async def test_already_paired(hass, mock_entry_setup): result["flow_id"], {"host": "123.123.123.123", "security_code": "abcd"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_authenticate"} @@ -53,7 +53,7 @@ async def test_user_connection_successful(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].data == { "host": "123.123.123.123", "gateway_id": "bla", @@ -74,7 +74,7 @@ async def test_user_connection_timeout(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "timeout"} @@ -92,7 +92,7 @@ async def test_user_connection_bad_key(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"security_code": "invalid_security_code"} @@ -120,7 +120,7 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "homekit-id" assert result["result"].data == { "host": "123.123.123.123", @@ -149,7 +149,7 @@ async def test_discovery_duplicate_aborted(hass): ), ) - assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["type"] == data_entry_flow.FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.data["host"] == "new-host" @@ -165,7 +165,7 @@ async def test_import_duplicate_aborted(hass): data={"host": "some-host"}, ) - assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["type"] == data_entry_flow.FlowResultType.ABORT assert flow["reason"] == "already_configured" @@ -185,7 +185,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( "tradfri", @@ -201,7 +201,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): ), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT async def test_discovery_updates_unique_id(hass): @@ -226,7 +226,7 @@ async def test_discovery_updates_unique_id(hass): ), ) - assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["type"] == data_entry_flow.FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.unique_id == "homekit-id" diff --git a/tests/components/trafikverket_ferry/test_config_flow.py b/tests/components/trafikverket_ferry/test_config_flow.py index c75937999d0..47befa55fed 100644 --- a/tests/components/trafikverket_ferry/test_config_flow.py +++ b/tests/components/trafikverket_ferry/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.trafikverket_ferry.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -50,7 +46,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Ekerö to Slagsta at 10:00" assert result2["data"] == { "api_key": "1234567890", @@ -91,7 +87,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -137,7 +133,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -152,7 +148,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -220,7 +216,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -235,7 +231,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index 37788fc285b..8a4baa18ec2 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.trafikverket_train.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -50,7 +46,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Stockholm C to Uppsala C at 10:00" assert result2["data"] == { "api_key": "1234567890", @@ -86,7 +82,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -107,7 +103,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -140,7 +136,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -165,7 +161,7 @@ async def test_flow_fails_incorrect_time(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result5["type"] == RESULT_TYPE_FORM + assert result5["type"] == FlowResultType.FORM assert result5["step_id"] == config_entries.SOURCE_USER with patch( @@ -210,7 +206,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -225,7 +221,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -297,7 +293,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -312,7 +308,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index 458652f8175..89fac5fd5df 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -8,7 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType DOMAIN = "trafikverket_weatherstation" CONF_STATION = "station" @@ -76,7 +76,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 7588736e997..24df92f536e 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -89,7 +89,7 @@ async def test_flow_user_config(hass, api): result = await hass.config_entries.flow.async_init( transmission.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -101,7 +101,7 @@ async def test_flow_required_fields(hass, api): data={CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -116,7 +116,7 @@ async def test_flow_all_provided(hass, api): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -137,10 +137,10 @@ async def test_options(hass): options_flow = flow.async_get_options_flow(entry) result = await options_flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 10 @@ -171,7 +171,7 @@ async def test_host_already_configured(hass, api): context={"source": config_entries.SOURCE_USER}, data=mock_entry_unique_port, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY mock_entry_unique_host = MOCK_ENTRY.copy() mock_entry_unique_host[CONF_HOST] = "192.168.1.101" @@ -181,7 +181,7 @@ async def test_host_already_configured(hass, api): context={"source": config_entries.SOURCE_USER}, data=mock_entry_unique_host, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_name_already_configured(hass, api): @@ -218,7 +218,7 @@ async def test_error_on_wrong_credentials(hass, auth_error): CONF_PORT: PORT, } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", @@ -238,7 +238,7 @@ async def test_error_on_connection_failure(hass, conn_error): CONF_PORT: PORT, } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -255,7 +255,7 @@ async def test_error_on_unknown_error(hass, unknown_error): CONF_PORT: PORT, } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 46db598e1d8..82e7bfa4b51 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -84,7 +84,7 @@ async def test_user_flow( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" tuya().connect = MagicMock(side_effect=side_effects) @@ -95,7 +95,7 @@ async def test_user_flow( country = [country for country in TUYA_COUNTRIES if country.name == MOCK_COUNTRY][0] - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_USERNAME assert result["data"][CONF_ACCESS_ID] == MOCK_ACCESS_ID assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET @@ -115,7 +115,7 @@ async def test_error_on_invalid_credentials(hass, tuya): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" tuya().connect = MagicMock(return_value=RESPONSE_ERROR) diff --git a/tests/components/twentemilieu/test_calendar.py b/tests/components/twentemilieu/test_calendar.py index b9f1dd9247d..11f8a1abd75 100644 --- a/tests/components/twentemilieu/test_calendar.py +++ b/tests/components/twentemilieu/test_calendar.py @@ -27,7 +27,7 @@ async def test_waste_pickup_calendar( assert entry.unique_id == "12345" assert state.attributes[ATTR_ICON] == "mdi:delete-empty" assert state.attributes["all_day"] is True - assert state.attributes["message"] == "Christmas Tree Pickup" + assert state.attributes["message"] == "Christmas tree pickup" assert not state.attributes["location"] assert not state.attributes["description"] assert state.state == STATE_OFF @@ -78,7 +78,7 @@ async def test_api_events( assert events[0] == { "start": {"date": "2022-01-06"}, "end": {"date": "2022-01-06"}, - "summary": "Christmas Tree Pickup", + "summary": "Christmas tree pickup", "description": None, "location": None, } diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index aec0f29e590..05e20dd06bd 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.twentemilieu.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -33,7 +29,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -46,7 +42,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_ID: 12345, @@ -70,7 +66,7 @@ async def test_invalid_address( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -83,7 +79,7 @@ async def test_invalid_address( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_address"} assert "flow_id" in result2 @@ -97,7 +93,7 @@ async def test_invalid_address( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_ID: 12345, @@ -124,7 +120,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert result.get("errors") == {"base": "cannot_connect"} @@ -146,5 +142,5 @@ async def test_address_already_set_up( }, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/twentemilieu/test_sensor.py b/tests/components/twentemilieu/test_sensor.py index 5f09018358e..6e20fd4d141 100644 --- a/tests/components/twentemilieu/test_sensor.py +++ b/tests/components/twentemilieu/test_sensor.py @@ -22,57 +22,72 @@ async def test_waste_pickup_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.christmas_tree_pickup") - entry = entity_registry.async_get("sensor.christmas_tree_pickup") + state = hass.states.get("sensor.twente_milieu_christmas_tree_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_christmas_tree_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_tree" assert state.state == "2022-01-06" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Christmas Tree Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Twente Milieu Christmas tree pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:pine-tree" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.non_recyclable_waste_pickup") - entry = entity_registry.async_get("sensor.non_recyclable_waste_pickup") + state = hass.states.get("sensor.twente_milieu_non_recyclable_waste_pickup") + entry = entity_registry.async_get( + "sensor.twente_milieu_non_recyclable_waste_pickup" + ) assert entry assert state assert entry.unique_id == "twentemilieu_12345_Non-recyclable" assert state.state == "2021-11-01" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Non-recyclable Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Twente Milieu Non-recyclable waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.organic_waste_pickup") - entry = entity_registry.async_get("sensor.organic_waste_pickup") + state = hass.states.get("sensor.twente_milieu_organic_waste_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_organic_waste_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_Organic" assert state.state == "2021-11-02" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Organic Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Twente Milieu Organic waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.packages_waste_pickup") - entry = entity_registry.async_get("sensor.packages_waste_pickup") + state = hass.states.get("sensor.twente_milieu_packages_waste_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_packages_waste_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_Plastic" assert state.state == "2021-11-03" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Packages Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Twente Milieu Packages waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.paper_waste_pickup") - entry = entity_registry.async_get("sensor.paper_waste_pickup") + state = hass.states.get("sensor.twente_milieu_paper_waste_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_paper_waste_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_Paper" assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Paper Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Twente Milieu Paper waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 8490f7541eb..d4fe42f10c7 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -14,10 +14,10 @@ async def test_config_flow_registers_webhook(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "twilio", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] twilio_events = [] diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 7369816fdc7..66945e972de 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -10,11 +10,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components.ukraine_alarm.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -60,10 +56,10 @@ async def test_state(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -77,7 +73,7 @@ async def test_state(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "State 1" assert result3["data"] == { "region": "1", @@ -91,10 +87,10 @@ async def test_state_district(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -102,7 +98,7 @@ async def test_state_district(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -116,7 +112,7 @@ async def test_state_district(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["title"] == "District 2.2" assert result4["data"] == { "region": "2.2", @@ -130,10 +126,10 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -141,7 +137,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -155,7 +151,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["title"] == "State 2" assert result4["data"] == { "region": "2", @@ -169,12 +165,12 @@ async def test_state_district_community(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -182,7 +178,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3", }, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM result4 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -190,7 +186,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3.2", }, ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -204,7 +200,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result5["type"] == RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == FlowResultType.CREATE_ENTRY assert result5["title"] == "Community 3.2.1" assert result5["data"] == { "region": "3.2.1", @@ -235,7 +231,7 @@ async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "rate_limit" @@ -247,7 +243,7 @@ async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -257,7 +253,7 @@ async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -269,7 +265,7 @@ async def test_unknown_client_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -279,7 +275,7 @@ async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "timeout" @@ -291,5 +287,5 @@ async def test_no_regions_returned( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index e774c5a551d..a1f6f3d4b02 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -95,7 +95,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "unifi", @@ -135,7 +135,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Site name" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -161,7 +161,7 @@ async def test_flow_works_negative_discovery(hass, aioclient_mock, mock_discover UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "", @@ -178,7 +178,7 @@ async def test_flow_multiple_sites(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -212,7 +212,7 @@ async def test_flow_multiple_sites(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "site" assert result["data_schema"]({"site": "1"}) assert result["data_schema"]({"site": "2"}) @@ -226,7 +226,7 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.clear_requests() @@ -261,7 +261,7 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -281,7 +281,7 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -315,7 +315,7 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "configuration_updated" @@ -325,7 +325,7 @@ async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -342,7 +342,7 @@ async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "faulty_credentials"} @@ -352,7 +352,7 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -369,7 +369,7 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "service_unavailable"} @@ -389,7 +389,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): data=config_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER aioclient_mock.clear_requests() @@ -424,7 +424,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_HOST] == "1.2.3.4" assert config_entry.data[CONF_USERNAME] == "new_name" @@ -447,7 +447,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device_tracker" assert not result["last_step"] assert set( @@ -465,7 +465,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "client_control" assert not result["last_step"] @@ -478,7 +478,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "statistics_sensors" assert result["last_step"] @@ -490,7 +490,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACK_CLIENTS: False, CONF_TRACK_WIRED_CLIENTS: False, @@ -521,7 +521,7 @@ async def test_simple_option_flow(hass, aioclient_mock): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "simple_options" assert result["last_step"] @@ -534,7 +534,7 @@ async def test_simple_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 8a41ada9b62..e420d031f46 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -30,7 +30,7 @@ from homeassistant.components.unifi.const import ( from homeassistant.components.unifi.controller import ( PLATFORMS, RETRY_TIMER, - get_controller, + get_unifi_controller, ) from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.const import ( @@ -271,7 +271,7 @@ async def test_controller_mac(hass, aioclient_mock): async def test_controller_not_accessible(hass): """Retry to login gets scheduled when connection fails.""" with patch( - "homeassistant.components.unifi.controller.get_controller", + "homeassistant.components.unifi.controller.get_unifi_controller", side_effect=CannotConnect, ): await setup_unifi_integration(hass) @@ -281,7 +281,7 @@ async def test_controller_not_accessible(hass): async def test_controller_trigger_reauth_flow(hass): """Failed authentication trigger a reauthentication flow.""" with patch( - "homeassistant.components.unifi.controller.get_controller", + "homeassistant.components.unifi.get_unifi_controller", side_effect=AuthenticationRequired, ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await setup_unifi_integration(hass) @@ -292,7 +292,7 @@ async def test_controller_trigger_reauth_flow(hass): async def test_controller_unknown_error(hass): """Unknown errors are handled.""" with patch( - "homeassistant.components.unifi.controller.get_controller", + "homeassistant.components.unifi.controller.get_unifi_controller", side_effect=Exception, ): await setup_unifi_integration(hass) @@ -379,7 +379,21 @@ async def test_wireless_client_event_calls_update_wireless_devices( hass, aioclient_mock, mock_unifi_websocket ): """Call update_wireless_devices method when receiving wireless client event.""" - await setup_unifi_integration(hass, aioclient_mock) + client_1_dict = { + "essid": "ssid", + "disabled": False, + "hostname": "client_1", + "ip": "10.0.0.4", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + } + await setup_unifi_integration( + hass, + aioclient_mock, + clients_response=[client_1_dict], + known_wireless_clients=(client_1_dict["mac"],), + ) with patch( "homeassistant.components.unifi.controller.UniFiController.update_wireless_clients", @@ -391,6 +405,7 @@ async def test_wireless_client_event_calls_update_wireless_devices( "data": [ { "datetime": "2020-01-20T19:37:04Z", + "user": "00:00:00:00:00:01", "key": aiounifi.events.WIRELESS_CLIENT_CONNECTED, "msg": "User[11:22:33:44:55:66] has connected to WLAN", "time": 1579549024893, @@ -455,22 +470,22 @@ async def test_reconnect_mechanism_exceptions( mock_reconnect.assert_called_once() -async def test_get_controller(hass): +async def test_get_unifi_controller(hass): """Successful call.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await get_controller(hass, **CONTROLLER_DATA) + assert await get_unifi_controller(hass, CONTROLLER_DATA) -async def test_get_controller_verify_ssl_false(hass): +async def test_get_unifi_controller_verify_ssl_false(hass): """Successful call with verify ssl set to false.""" controller_data = dict(CONTROLLER_DATA) controller_data[CONF_VERIFY_SSL] = False with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await get_controller(hass, **controller_data) + assert await get_unifi_controller(hass, controller_data) @pytest.mark.parametrize( @@ -486,9 +501,11 @@ async def test_get_controller_verify_ssl_false(hass): (aiounifi.AiounifiException, AuthenticationRequired), ], ) -async def test_get_controller_fails_to_connect(hass, side_effect, raised_exception): - """Check that get_controller can handle controller being unavailable.""" +async def test_get_unifi_controller_fails_to_connect( + hass, side_effect, raised_exception +): + """Check that get_unifi_controller can handle controller being unavailable.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", side_effect=side_effect ), pytest.raises(raised_exception): - await get_controller(hass, **CONTROLLER_DATA) + await get_unifi_controller(hass, CONTROLLER_DATA) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index f183e1c22ff..03ea89097c5 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,10 +1,10 @@ """Test UniFi Network integration setup process.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from homeassistant.components import unifi from homeassistant.components.unifi import async_flatten_entry_data from homeassistant.components.unifi.const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN -from homeassistant.helpers import device_registry as dr +from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.setup import async_setup_component from .test_controller import ( @@ -29,40 +29,27 @@ async def test_successful_config_entry(hass, aioclient_mock): assert hass.data[UNIFI_DOMAIN] -async def test_controller_fail_setup(hass): - """Test that a failed setup still stores controller.""" - with patch("homeassistant.components.unifi.UniFiController") as mock_controller: - mock_controller.return_value.async_setup = AsyncMock(return_value=False) +async def test_setup_entry_fails_config_entry_not_ready(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.unifi.get_unifi_controller", + side_effect=CannotConnect, + ): await setup_unifi_integration(hass) assert hass.data[UNIFI_DOMAIN] == {} -async def test_controller_mac(hass): - """Test that configured options for a host are loaded via config entry.""" - entry = MockConfigEntry( - domain=UNIFI_DOMAIN, data=ENTRY_CONFIG, unique_id="1", entry_id=1 - ) - entry.add_to_hass(hass) +async def test_setup_entry_fails_trigger_reauth_flow(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.unifi.get_unifi_controller", + side_effect=AuthenticationRequired, + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await setup_unifi_integration(hass) + mock_flow_init.assert_called_once() - with patch("homeassistant.components.unifi.UniFiController") as mock_controller: - mock_controller.return_value.async_setup = AsyncMock(return_value=True) - mock_controller.return_value.mac = "mac1" - mock_controller.return_value.api.url = "https://123:443" - assert await unifi.async_setup_entry(hass, entry) is True - - assert len(mock_controller.mock_calls) == 2 - - device_registry = dr.async_get(hass) - device = device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "mac1")}, - ) - assert device.configuration_url == "https://123:443" - assert device.manufacturer == "Ubiquiti Networks" - assert device.model == "UniFi Network" - assert device.name == "UniFi Network" - assert device.sw_version is None + assert hass.data[UNIFI_DOMAIN] == {} async def test_flatten_entry_data(hass): diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index b5eb9c1d02e..677491319a1 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -1,23 +1,60 @@ """The tests for the UniFi Network update platform.""" +from copy import deepcopy from aiounifi.controller import MESSAGE_DEVICE from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING +from yarl import URL +from homeassistant.components.unifi.const import CONF_SITE_ID from homeassistant.components.update import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, UpdateDeviceClass, + UpdateEntityFeature, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_HOST, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) -from .test_controller import setup_unifi_integration +from .test_controller import DESCRIPTION, setup_unifi_integration + +DEVICE_1 = { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device 1", + "next_interval": 20, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + "upgrade_to_firmware": "4.3.17.11279", +} + +DEVICE_2 = { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.1.2", + "mac": "00:00:00:00:01:02", + "model": "US16P150", + "name": "Device 2", + "next_interval": 20, + "state": 0, + "type": "usw", + "version": "4.0.42.10433", +} async def test_no_entities(hass, aioclient_mock): @@ -31,41 +68,11 @@ async def test_device_updates( hass, aioclient_mock, mock_unifi_websocket, mock_device_registry ): """Test the update_items function with some devices.""" - device_1 = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device 1", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - "upgrade_to_firmware": "4.3.17.11279", - } - device_2 = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "ip": "10.0.1.2", - "mac": "00:00:00:00:01:02", - "model": "US16P150", - "name": "Device 2", - "next_interval": 20, - "state": 0, - "type": "usw", - "version": "4.0.42.10433", - } + device_1 = deepcopy(DEVICE_1) await setup_unifi_integration( hass, aioclient_mock, - devices_response=[device_1, device_2], + devices_response=[device_1, DEVICE_2], ) assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 2 @@ -76,6 +83,10 @@ async def test_device_updates( assert device_1_state.attributes[ATTR_LATEST_VERSION] == "4.3.17.11279" assert device_1_state.attributes[ATTR_IN_PROGRESS] is False assert device_1_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert ( + device_1_state.attributes[ATTR_SUPPORTED_FEATURES] + == UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL + ) device_2_state = hass.states.get("update.device_2") assert device_2_state.state == STATE_OFF @@ -83,6 +94,10 @@ async def test_device_updates( assert device_2_state.attributes[ATTR_LATEST_VERSION] == "4.0.42.10433" assert device_2_state.attributes[ATTR_IN_PROGRESS] is False assert device_2_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert ( + device_2_state.attributes[ATTR_SUPPORTED_FEATURES] + == UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL + ) # Simulate start of update @@ -122,46 +137,78 @@ async def test_device_updates( assert device_1_state.attributes[ATTR_IN_PROGRESS] is False -async def test_controller_state_change( - hass, aioclient_mock, mock_unifi_websocket, mock_device_registry -): - """Verify entities state reflect on controller becoming unavailable.""" - device = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - "upgrade_to_firmware": "4.3.17.11279", - } +async def test_not_admin(hass, aioclient_mock): + """Test that the INSTALL feature is not available on a non-admin account.""" + description = deepcopy(DESCRIPTION) + description[0]["site_role"] = "not admin" await setup_unifi_integration( hass, aioclient_mock, - devices_response=[device], + site_description=description, + devices_response=[DEVICE_1], ) assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 - assert hass.states.get("update.device").state == STATE_ON + device_state = hass.states.get("update.device_1") + assert device_state.state == STATE_ON + assert ( + device_state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature.PROGRESS + ) + + +async def test_install(hass, aioclient_mock): + """Test the device update install call.""" + config_entry = await setup_unifi_integration( + hass, aioclient_mock, devices_response=[DEVICE_1] + ) + + assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 + device_state = hass.states.get("update.device_1") + assert device_state.state == STATE_ON + + url = f"https://{config_entry.data[CONF_HOST]}:1234/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr" + aioclient_mock.clear_requests() + aioclient_mock.post(url) + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.device_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0] == ( + "post", + URL(url), + {"cmd": "upgrade", "mac": "00:00:00:00:01:01"}, + {}, + ) + + +async def test_controller_state_change( + hass, aioclient_mock, mock_unifi_websocket, mock_device_registry +): + """Verify entities state reflect on controller becoming unavailable.""" + await setup_unifi_integration( + hass, + aioclient_mock, + devices_response=[DEVICE_1], + ) + + assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 + assert hass.states.get("update.device_1").state == STATE_ON # Controller unavailable mock_unifi_websocket(state=STATE_DISCONNECTED) await hass.async_block_till_done() - assert hass.states.get("update.device").state == STATE_UNAVAILABLE + assert hass.states.get("update.device_1").state == STATE_UNAVAILABLE # Controller available mock_unifi_websocket(state=STATE_RUNNING) await hass.async_block_till_done() - assert hass.states.get("update.device").state == STATE_ON + assert hass.states.get("update.device_1").state == STATE_ON diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 51cef190e2f..2a9edb605e7 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta +from functools import partial from ipaddress import IPv4Address import json from typing import Any @@ -102,6 +103,11 @@ def mock_ufp_client(bootstrap: Bootstrap): """Mock ProtectApiClient for testing.""" client = Mock() client.bootstrap = bootstrap + client._bootstrap = bootstrap + client.api_path = "/api" + # functionality from API client tests actually need + client._stream_response = partial(ProtectApiClient._stream_response, client) + client.get_camera_video = partial(ProtectApiClient.get_camera_video, client) nvr = client.bootstrap.nvr nvr._api = client diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 3d561f2d781..5304e05fe13 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -19,11 +19,7 @@ from homeassistant.components.unifiprotect.const import ( ) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr from . import ( @@ -66,7 +62,7 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch( @@ -86,7 +82,7 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "1.1.1.1", @@ -118,7 +114,7 @@ async def test_form_version_too_old(hass: HomeAssistant, old_nvr: NVR) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "protect_version"} @@ -141,7 +137,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -164,7 +160,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -191,7 +187,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: "entry_id": mock_config.entry_id, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -211,7 +207,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} assert result2["step_id"] == "reauth_confirm" @@ -227,7 +223,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: }, ) - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -258,7 +254,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - assert mock_config.state == config_entries.ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "init" @@ -267,7 +263,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - {CONF_DISABLE_RTSP: True, CONF_ALL_UPDATES: True, CONF_OVERRIDE_CHOST: True}, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == { "all_updates": True, "disable_rtsp": True, @@ -295,7 +291,7 @@ async def test_discovered_by_ssdp_or_dhcp( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "discovery_started" @@ -312,7 +308,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -338,7 +334,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DIRECT_CONNECT_DOMAIN, @@ -378,7 +374,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == DIRECT_CONNECT_DOMAIN @@ -413,7 +409,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "127.0.0.1" @@ -448,7 +444,7 @@ async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_ ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "1.2.2.2" @@ -479,7 +475,7 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "a.hostname" @@ -495,7 +491,7 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -521,7 +517,7 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -547,7 +543,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -573,7 +569,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -612,7 +608,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -642,7 +638,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -680,7 +676,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -716,7 +712,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -742,7 +738,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "nomatchsameip.ui.direct", @@ -785,7 +781,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -806,5 +802,5 @@ async def test_discovery_can_be_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py new file mode 100644 index 00000000000..e64a0a87377 --- /dev/null +++ b/tests/components/unifiprotect/test_views.py @@ -0,0 +1,427 @@ +"""Test UniFi Protect views.""" + +from datetime import datetime, timedelta +from typing import Any, cast +from unittest.mock import AsyncMock, Mock + +from aiohttp import ClientResponse +import pytest +from pyunifiprotect.data import Camera, Event, EventType +from pyunifiprotect.exceptions import ClientError + +from homeassistant.components.unifiprotect.views import ( + async_generate_event_video_url, + async_generate_thumbnail_url, +) +from homeassistant.core import HomeAssistant + +from .utils import MockUFPFixture, init_entry + +from tests.test_util.aiohttp import mock_aiohttp_client + + +async def test_thumbnail_bad_nvr_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid NVR ID in URL.""" + + ufp.api.get_event_thumbnail = AsyncMock() + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_event_thumbnail.assert_not_called + + +@pytest.mark.parametrize("width,height", [("test", None), (None, "test")]) +async def test_thumbnail_bad_params( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + width: Any, + height: Any, +) -> None: + """Test invalid bad query parameters.""" + + ufp.api.get_event_thumbnail = AsyncMock() + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url( + "test_id", ufp.api.bootstrap.nvr.id, width=width, height=height + ) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 400 + ufp.api.get_event_thumbnail.assert_not_called + + +async def test_thumbnail_bad_event( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid with error raised.""" + + ufp.api.get_event_thumbnail = AsyncMock(side_effect=ClientError()) + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + +async def test_thumbnail_no_data( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid no thumbnail returned.""" + + ufp.api.get_event_thumbnail = AsyncMock(return_value=None) + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + +async def test_thumbnail( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid NVR ID in URL.""" + + ufp.api.get_event_thumbnail = AsyncMock(return_value=b"testtest") + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 200 + assert response.content_type == "image/jpeg" + assert await response.content.read() == b"testtest" + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + +async def test_video_bad_event( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test generating event with bad camera ID.""" + + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id="test_id", + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + with pytest.raises(ValueError): + async_generate_event_video_url(event) + + +async def test_video_bad_event_ongoing( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test generating event with bad camera ID.""" + + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=None, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + with pytest.raises(ValueError): + async_generate_event_video_url(event) + + +async def test_video_bad_perms( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test generating event with bad user permissions.""" + + ufp.api.bootstrap.auth_user.all_permissions = [] + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + with pytest.raises(PermissionError): + async_generate_event_video_url(event) + + +async def test_video_bad_nvr_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with bad NVR id.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + url = url.replace(ufp.api.bootstrap.nvr.id, "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_not_called + + +async def test_video_bad_camera_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with bad camera id.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + url = url.replace(camera.id, "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_not_called + + +async def test_video_bad_camera_perms( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with bad camera perms.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + + ufp.api.bootstrap.auth_user.all_permissions = [] + ufp.api.bootstrap.auth_user._perm_cache = {} + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 403 + ufp.api.request.assert_not_called + + +@pytest.mark.parametrize("start,end", [("test", None), (None, "test")]) +async def test_video_bad_params( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, + start: Any, + end: Any, +) -> None: + """Test video URL with bad start/end params.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + from_value = event_start if start is not None else fixed_now + to_value = start if start is not None else end + url = url.replace(from_value.isoformat(), to_value) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 400 + ufp.api.request.assert_not_called + + +async def test_video_bad_video( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with no video.""" + + ufp.api.request = AsyncMock(side_effect=ClientError) + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_called_once + + +async def test_video( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with no video.""" + + content = Mock() + content.__anext__ = AsyncMock(side_effect=[b"test", b"test", StopAsyncIteration()]) + content.__aiter__ = Mock(return_value=content) + + mock_response = Mock() + mock_response.content_length = 8 + mock_response.content.iter_chunked = Mock(return_value=content) + + ufp.api.request = AsyncMock(return_value=mock_response) + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + assert await response.content.read() == b"testtest" + + assert response.status == 200 + ufp.api.request.assert_called_once diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 260c6996128..8d54cbddea6 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -2,9 +2,10 @@ # pylint: disable=protected-access from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass from datetime import timedelta -from typing import Any, Callable, Sequence +from typing import Any, Callable from unittest.mock import Mock from pyunifiprotect import ProtectApiClient diff --git a/tests/components/upcloud/test_config_flow.py b/tests/components/upcloud/test_config_flow.py index 7fce853b5d3..4bbc7c51b9d 100644 --- a/tests/components/upcloud/test_config_flow.py +++ b/tests/components/upcloud/test_config_flow.py @@ -26,7 +26,7 @@ async def test_show_set_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -37,7 +37,7 @@ async def test_connection_error(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -56,7 +56,7 @@ async def test_login_error(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -68,7 +68,7 @@ async def test_success(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -82,7 +82,7 @@ async def test_options(hass): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 66d84fe0862..e89b8274c18 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -45,7 +45,7 @@ async def test_flow_ssdp(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -53,7 +53,7 @@ async def test_flow_ssdp(hass: HomeAssistant): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -81,7 +81,7 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "incomplete_discovery" @@ -102,7 +102,7 @@ async def test_flow_ssdp_non_igd_device(hass: HomeAssistant): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "non_igd_device" @@ -120,7 +120,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -128,7 +128,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -167,7 +167,7 @@ async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "config_entry_updated" @@ -207,7 +207,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs(hass: HomeAssistan context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # UDN + ST different: New discovery via step ssdp. @@ -225,7 +225,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs(hass: HomeAssistan context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" @@ -256,7 +256,7 @@ async def test_flow_ssdp_discovery_changed_location(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test if location is updated. @@ -285,7 +285,7 @@ async def test_flow_ssdp_discovery_ignored_entry(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -316,7 +316,7 @@ async def test_flow_ssdp_discovery_changed_udn_ignored_entry(hass: HomeAssistant context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "discovery_ignored" @@ -332,7 +332,7 @@ async def test_flow_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Confirmed via step user. @@ -340,7 +340,7 @@ async def test_flow_user(hass: HomeAssistant): result["flow_id"], user_input={"unique_id": TEST_USN}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -362,5 +362,5 @@ async def test_flow_user_no_discovery(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/uptime/test_config_flow.py b/tests/components/uptime/test_config_flow.py index 69ba00f6ac8..9db909003e9 100644 --- a/tests/components/uptime/test_config_flow.py +++ b/tests/components/uptime/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.uptime.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +21,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -34,7 +30,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Uptime" assert result2.get("data") == {} @@ -52,7 +48,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -67,6 +63,6 @@ async def test_import_flow( data={CONF_NAME: "My Uptime"}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "My Uptime" assert result.get("data") == {} diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index e8d0306246f..053224c3b4f 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -2,9 +2,10 @@ import pytest from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.uptime.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -25,3 +26,11 @@ async def test_uptime_sensor( entry = entity_registry.async_get("sensor.uptime") assert entry assert entry.unique_id == init_integration.entry_id + + device_registry = dr.async_get(hass) + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, init_integration.entry_id)} + assert device_entry.name == init_integration.title + assert device_entry.entry_type == dr.DeviceEntryType.SERVICE diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index c477e1bbc65..b058ceecbdc 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant import config_entries from homeassistant.components.uptimerobot.const import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .common import ( MOCK_UPTIMEROBOT_ACCOUNT, @@ -34,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -51,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["result"].unique_id == MOCK_UPTIMEROBOT_UNIQUE_ID - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_UPTIMEROBOT_ACCOUNT["email"] assert result2["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +59,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -76,7 +72,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == "not_main_key" @@ -103,7 +99,7 @@ async def test_form_exception_thrown(hass: HomeAssistant, exception, error_key) {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == error_key @@ -136,7 +132,7 @@ async def test_user_unique_id_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -153,7 +149,7 @@ async def test_user_unique_id_already_exists( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -174,7 +170,7 @@ async def test_reauthentication( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -192,7 +188,7 @@ async def test_reauthentication( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -213,7 +209,7 @@ async def test_reauthentication_failure( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -232,7 +228,7 @@ async def test_reauthentication_failure( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == "unknown" @@ -255,7 +251,7 @@ async def test_reauthentication_failure_no_existing_entry( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -273,7 +269,7 @@ async def test_reauthentication_failure_no_existing_entry( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_failed_existing" @@ -294,7 +290,7 @@ async def test_reauthentication_failure_account_not_matching( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -316,5 +312,5 @@ async def test_reauthentication_failure_account_not_matching( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == "reauth_failed_matching_account" diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 53f9d814f2f..29a8cab9677 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.utility_meter.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -38,7 +38,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -73,7 +73,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -88,7 +88,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -117,7 +117,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -132,7 +132,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"]["base"] == "tariffs_not_unique" @@ -172,7 +172,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "source") == input_sensor1_entity_id @@ -181,7 +181,7 @@ async def test_options(hass: HomeAssistant) -> None: result["flow_id"], user_input={"source": input_sensor2_entity_id}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "cycle": "monthly", "delta_values": False, diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py index ee6b05f1f8b..b0c951383b8 100644 --- a/tests/components/vallox/test_config_flow.py +++ b/tests/components/vallox/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.vallox.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +19,7 @@ async def test_form_no_input(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None @@ -33,7 +29,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert init["type"] == RESULT_TYPE_FORM + assert init["type"] == FlowResultType.FORM assert init["errors"] is None with patch( @@ -49,7 +45,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Vallox" assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} assert len(mock_setup_entry.mock_calls) == 1 @@ -67,7 +63,7 @@ async def test_form_invalid_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "invalid_host"} @@ -87,7 +83,7 @@ async def test_form_vallox_api_exception_cannot_connect(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -107,7 +103,7 @@ async def test_form_os_error_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -127,7 +123,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "unknown"} @@ -152,7 +148,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -174,7 +170,7 @@ async def test_import_with_custom_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == name assert result["data"] == {"host": "1.2.3.4", "name": "Vallox 90 MV"} assert len(mock_setup_entry.mock_calls) == 1 @@ -196,7 +192,7 @@ async def test_import_without_custom_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Vallox" assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} assert len(mock_setup_entry.mock_calls) == 1 @@ -213,7 +209,7 @@ async def test_import_invalid_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_host" @@ -237,7 +233,7 @@ async def test_import_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -256,7 +252,7 @@ async def test_import_cannot_connect_os_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -275,7 +271,7 @@ async def test_import_cannot_connect_vallox_api_exception(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -294,5 +290,5 @@ async def test_import_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 960eedcbd01..207f745e495 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -66,20 +66,20 @@ async def test_user(hass: HomeAssistant): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user( {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "velbus_test_serial" assert result["data"][CONF_PORT] == PORT_SERIAL result = await flow.async_step_user( {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "velbus_test_tcp" assert result["data"][CONF_PORT] == PORT_TCP @@ -92,13 +92,13 @@ async def test_user_fail(hass: HomeAssistant): result = await flow.async_step_user( {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PORT: "cannot_connect"} result = await flow.async_step_user( {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PORT: "cannot_connect"} @@ -108,7 +108,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): flow = init_config_flow(hass) result = await flow.async_step_user({CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"port": "already_configured"} @@ -121,14 +121,14 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # test an already configured discovery entry = MockConfigEntry( @@ -141,7 +141,7 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -154,5 +154,5 @@ async def test_flow_usb_failed(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/venstar/test_config_flow.py b/tests/components/venstar/test_config_flow.py index f568655ec8d..f8f66f1b388 100644 --- a/tests/components/venstar/test_config_flow.py +++ b/tests/components/venstar/test_config_flow.py @@ -13,11 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import VenstarColorTouchMock @@ -41,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -57,7 +53,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -77,7 +73,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -96,7 +92,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -108,7 +104,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -124,5 +120,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 780583e38ab..759a9049969 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.vera import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, mock_registry @@ -23,7 +23,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result = await hass.config_entries.flow.async_configure( @@ -34,7 +34,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: CONF_EXCLUDE: "14 15", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -63,7 +63,7 @@ async def test_async_step_import_success(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -94,7 +94,7 @@ async def test_async_step_import_success_with_legacy_unique_id( data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -138,7 +138,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -148,7 +148,7 @@ async def test_options(hass): CONF_EXCLUDE: "8,9;10 11 12_13bb14", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], diff --git a/tests/components/verisure/conftest.py b/tests/components/verisure/conftest.py new file mode 100644 index 00000000000..f91215866d8 --- /dev/null +++ b/tests/components/verisure/conftest.py @@ -0,0 +1,50 @@ +"""Fixtures for Verisure integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.verisure.const import CONF_GIID, DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_GIID: "12345", + CONF_PASSWORD: "SuperS3cr3t!", + }, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.verisure.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_verisure_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Tailscale client.""" + with patch( + "homeassistant.components.verisure.config_flow.Verisure", autospec=True + ) as verisure_mock: + verisure = verisure_mock.return_value + verisure.login.return_value = True + verisure.installations = [ + {"giid": "12345", "alias": "ascending", "street": "12345th street"}, + {"giid": "54321", "alias": "descending", "street": "54321th street"}, + ] + yield verisure diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 356cd8fd63c..43adc91c38c 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Verisure config flow.""" from __future__ import annotations -from unittest.mock import PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from verisure import Error as VerisureError, LoginError as VerisureLoginError @@ -17,159 +17,328 @@ from homeassistant.components.verisure.const import ( ) from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -TEST_INSTALLATIONS = [ - {"giid": "12345", "alias": "ascending", "street": "12345th street"}, - {"giid": "54321", "alias": "descending", "street": "54321th street"}, -] -TEST_INSTALLATION = [TEST_INSTALLATIONS[0]] - -async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: +async def test_full_user_flow_single_installation( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: """Test a full user initiated configuration flow with a single installation.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATION - ) - mock_verisure.login.return_value = True + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "ascending (12345th street)" - assert result2["data"] == { + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("title") == "ascending (12345th street)" + assert result2.get("data") == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> None: +async def test_full_user_flow_multiple_installations( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: """Test a full user initiated configuration flow with multiple installations.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATIONS - ) - mock_verisure.login.return_value = True + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + assert result2.get("step_id") == "installation" + assert result2.get("type") == FlowResultType.FORM + assert result2.get("errors") is None + assert "flow_id" in result2 - assert result2["step_id"] == "installation" - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] is None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {"giid": "54321"} + ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], {"giid": "54321"} - ) - await hass.async_block_till_done() - - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY - assert result3["title"] == "descending (54321th street)" - assert result3["data"] == { + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "descending (54321th street)" + assert result3.get("data") == { CONF_GIID: "54321", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_invalid_login(hass: HomeAssistant) -> None: +async def test_full_user_flow_single_installation_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: + """Test a full user initiated flow with a single installation and mfa.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "ascending (12345th street)" + assert result3.get("data") == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_user_flow_multiple_installations_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: + """Test a full user initiated configuration flow with a single installation.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("step_id") == "installation" + assert result3.get("type") == FlowResultType.FORM + assert result3.get("errors") is None + assert "flow_id" in result2 + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], {"giid": "54321"} + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("title") == "descending (54321th street)" + assert result4.get("data") == { + CONF_GIID: "54321", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,error", + [ + (VerisureLoginError, "invalid_auth"), + (VerisureError, "unknown"), + ], +) +async def test_verisure_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + side_effect: Exception, + error: str, +) -> None: """Test a flow with an invalid Verisure My Pages login.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureLoginError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + assert "flow_id" in result - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_unknown_error(hass: HomeAssistant) -> None: - """Test a flow with an invalid Verisure My Pages login.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + mock_verisure_config_flow.login.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "user" + assert result2.get("errors") == {"base": error} + assert "flow_id" in result2 - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + mock_verisure_config_flow.login_mfa.side_effect = side_effect + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + mock_verisure_config_flow.login_mfa.side_effect = None + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "user" + assert result3.get("errors") == {"base": "unknown_mfa"} + assert "flow_id" in result3 + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.FORM + assert result4.get("step_id") == "mfa" + assert "flow_id" in result4 + + mock_verisure_config_flow.mfa_validate.side_effect = side_effect + + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + "code": "123456", + }, + ) + assert result5.get("type") == FlowResultType.FORM + assert result5.get("step_id") == "mfa" + assert result5.get("errors") == {"base": error} + assert "flow_id" in result5 + + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + mock_verisure_config_flow.mfa_validate.side_effect = None + mock_verisure_config_flow.login.side_effect = None + + result6 = await hass.config_entries.flow.async_configure( + result5["flow_id"], + { + "code": "654321", + }, + ) + await hass.async_block_till_done() + + assert result6.get("type") == FlowResultType.CREATE_ENTRY + assert result6.get("title") == "ascending (12345th street)" + assert result6.get("data") == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 4 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 + assert len(mock_setup_entry.mock_calls) == 1 async def test_dhcp(hass: HomeAssistant) -> None: @@ -182,144 +351,234 @@ async def test_dhcp(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" -async def test_reauth_flow(hass: HomeAssistant) -> None: +async def test_reauth_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: """Test a reauthentication flow.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, }, - data=entry.data, + data=mock_config_entry.data, ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} + assert result.get("step_id") == "reauth_confirm" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - return_value=True, - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "correct horse battery staple", - }, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple", + }, + ) + await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT - assert result2["reason"] == "reauth_successful" - assert entry.data == { + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "reauth_successful" + assert mock_config_entry.data == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "correct horse battery staple", } - assert len(mock_verisure.mock_calls) == 1 + assert len(mock_verisure_config_flow.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None: +async def test_reauth_flow_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: """Test a reauthentication flow.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, }, - data=entry.data, + data=mock_config_entry.data, + ) + assert result.get("step_id") == "reauth_confirm" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" ) - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureLoginError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "WrOngP4ssw0rd!", - }, - ) - await hass.async_block_till_done() - - assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_reauth_flow_unknown_error(hass: HomeAssistant) -> None: - """Test a reauthentication flow, with an unknown error happening.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_PASSWORD: "SuperS3cr3t!", + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple!", }, ) - entry.add_to_hass(hass) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "reauth_mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.ABORT + assert result3.get("reason") == "reauth_successful" + assert mock_config_entry.data == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "correct horse battery staple!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,error", + [ + (VerisureLoginError, "invalid_auth"), + (VerisureError, "unknown"), + ], +) +async def test_reauth_flow_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + error: str, +) -> None: + """Test a reauthentication flow.""" + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, }, - data=entry.data, + data=mock_config_entry.data, ) - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "WrOngP4ssw0rd!", - }, - ) - await hass.async_block_till_done() + assert "flow_id" in result - assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} + mock_verisure_config_flow.login.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "WrOngP4ssw0rd!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("step_id") == "reauth_confirm" + assert result2.get("type") == FlowResultType.FORM + assert result2.get("errors") == {"base": error} + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + mock_verisure_config_flow.login_mfa.side_effect = side_effect + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "reauth_confirm" + assert result3.get("errors") == {"base": "unknown_mfa"} + assert "flow_id" in result3 + + mock_verisure_config_flow.login_mfa.side_effect = None + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.FORM + assert result4.get("step_id") == "reauth_mfa" + assert "flow_id" in result4 + + mock_verisure_config_flow.mfa_validate.side_effect = side_effect + + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + "code": "123456", + }, + ) + assert result5.get("type") == FlowResultType.FORM + assert result5.get("step_id") == "reauth_mfa" + assert result5.get("errors") == {"base": error} + assert "flow_id" in result5 + + mock_verisure_config_flow.mfa_validate.side_effect = None + mock_verisure_config_flow.login.side_effect = None + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + await hass.config_entries.flow.async_configure( + result5["flow_id"], + { + "code": "654321", + }, + ) + await hass.async_block_till_done() + + assert mock_config_entry.data == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 4 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 + assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( @@ -366,16 +625,17 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "init" + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == output + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("data") == output async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: @@ -396,9 +656,10 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "init" - assert result["errors"] == {} + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert result.get("errors") == {} + assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -408,6 +669,6 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "init" - assert result["errors"] == {"base": "code_format_mismatch"} + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert result.get("errors") == {"base": "code_format_mismatch"} diff --git a/tests/components/version/test_config_flow.py b/tests/components/version/test_config_flow.py index 64296498f35..9745272626c 100644 --- a/tests/components/version/test_config_flow.py +++ b/tests/components/version/test_config_flow.py @@ -19,7 +19,7 @@ from homeassistant.components.version.const import ( ) from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from homeassistant.util import dt from .common import MOCK_VERSION, MOCK_VERSION_DATA, setup_version_integration @@ -50,7 +50,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": False}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.version.async_setup_entry", @@ -62,7 +62,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == VERSION_SOURCE_DOCKER_HUB assert result2["data"] == { **DEFAULT_CONFIGURATION, @@ -78,7 +78,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -86,11 +86,11 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -102,7 +102,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_PYPI assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -119,7 +119,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -127,11 +127,11 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -143,7 +143,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_DOCKER_HUB assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -160,7 +160,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -168,11 +168,11 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -185,7 +185,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"{VERSION_SOURCE_VERSIONS} Dev" assert result["data"] == { **DEFAULT_CONFIGURATION, diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index cd68a0b5877..a1f9914ed67 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -17,7 +17,7 @@ async def test_abort_already_setup(hass): ) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -29,7 +29,7 @@ async def test_invalid_login_error(hass): with patch("pyvesync.vesync.VeSync.login", return_value=False): result = await flow.async_step_user(user_input=test_dict) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -38,11 +38,11 @@ async def test_config_flow_user_input(hass): flow = config_flow.VeSyncFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch("pyvesync.vesync.VeSync.login", return_value=True): result = await flow.async_step_user( {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == "user" assert result["data"][CONF_PASSWORD] == "pass" diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index 3096f8a492c..d2852b33606 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert len(result["errors"]) == 0 with patch( @@ -38,7 +38,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "ViCare" assert result2["data"] == ENTRY_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -64,7 +64,7 @@ async def test_invalid_login(hass) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -81,7 +81,7 @@ async def test_form_dhcp(hass): macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -102,7 +102,7 @@ async def test_form_dhcp(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "ViCare" assert result2["data"] == ENTRY_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -125,7 +125,7 @@ async def test_dhcp_single_instance_allowed(hass): macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -141,5 +141,5 @@ async def test_user_input_single_instance_allowed(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index 571399949b4..308431782f3 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -15,7 +15,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch("vilfo.Client.ping", return_value=None), patch( @@ -29,7 +29,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "testadmin.vilfo.com" assert result2["data"] == { "host": "testadmin.vilfo.com", @@ -56,7 +56,7 @@ async def test_form_invalid_auth(hass): {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -74,7 +74,7 @@ async def test_form_cannot_connect(hass): {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), patch( @@ -85,7 +85,7 @@ async def test_form_cannot_connect(hass): {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -128,8 +128,8 @@ async def test_form_already_configured(hass): {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, ) - assert first_flow_result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert second_flow_result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert first_flow_result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert second_flow_result2["type"] == data_entry_flow.FlowResultType.ABORT assert second_flow_result2["reason"] == "already_configured" diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 3250163ef8e..dd338be1321 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -66,14 +66,14 @@ async def test_user_flow_minimum_fields( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -91,14 +91,14 @@ async def test_user_flow_all_fields( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -117,19 +117,19 @@ async def test_speaker_options_flow( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -145,12 +145,12 @@ async def test_tv_options_flow_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -160,7 +160,7 @@ async def test_tv_options_flow_no_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -176,12 +176,12 @@ async def test_tv_options_flow_with_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -191,7 +191,7 @@ async def test_tv_options_flow_with_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -208,13 +208,13 @@ async def test_tv_options_flow_start_with_volume( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init( entry.entry_id, data={CONF_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP} @@ -223,7 +223,7 @@ async def test_tv_options_flow_start_with_volume( result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -233,7 +233,7 @@ async def test_tv_options_flow_start_with_volume( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -260,7 +260,7 @@ async def test_user_host_already_configured( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -284,7 +284,7 @@ async def test_user_serial_number_already_exists( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -296,7 +296,7 @@ async def test_user_error_on_could_not_connect( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -308,7 +308,7 @@ async def test_user_error_on_could_not_connect_invalid_token( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -323,19 +323,19 @@ async def test_user_tv_pairing_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pairing_complete" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -354,7 +354,7 @@ async def test_user_start_pairing_failure( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -370,14 +370,14 @@ async def test_user_invalid_pin( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" assert result["errors"] == {CONF_PIN: "complete_pairing_failed"} @@ -399,7 +399,7 @@ async def test_user_ignore( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_import_flow_minimum_fields( @@ -416,7 +416,7 @@ async def test_import_flow_minimum_fields( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST @@ -436,7 +436,7 @@ async def test_import_flow_all_fields( data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -463,7 +463,7 @@ async def test_import_entity_already_configured( DOMAIN, context={"source": SOURCE_IMPORT}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" @@ -481,7 +481,7 @@ async def test_import_flow_update_options( await hass.async_block_till_done() assert result["result"].options == {CONF_VOLUME_STEP: DEFAULT_VOLUME_STEP} - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_SPEAKER_CONFIG.copy() @@ -492,7 +492,7 @@ async def test_import_flow_update_options( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 @@ -512,7 +512,7 @@ async def test_import_flow_update_name_and_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy() @@ -524,7 +524,7 @@ async def test_import_flow_update_name_and_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.data[CONF_NAME] == NAME2 @@ -546,7 +546,7 @@ async def test_import_flow_update_remove_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS in config_entry.options @@ -559,7 +559,7 @@ async def test_import_flow_update_remove_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" assert CONF_APPS not in config_entry.data assert CONF_APPS not in config_entry.options @@ -576,26 +576,26 @@ async def test_import_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -616,7 +616,7 @@ async def test_import_with_apps_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=import_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Mock inputting info without apps to make sure apps get stored @@ -625,19 +625,19 @@ async def test_import_with_apps_needs_pairing( user_input=_get_config_schema(MOCK_TV_CONFIG_NO_TOKEN)(MOCK_TV_CONFIG_NO_TOKEN), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -659,7 +659,7 @@ async def test_import_flow_additional_configs( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS not in config_entry.options @@ -688,7 +688,7 @@ async def test_import_error( data=vol.Schema(VIZIO_SCHEMA)(fail_entry), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Ensure error gets logged vizio_log_list = [ @@ -719,7 +719,7 @@ async def test_import_ignore( data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_zeroconf_flow( @@ -735,7 +735,7 @@ async def test_zeroconf_flow( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Apply discovery updates to entry to mimic when user hits submit without changing @@ -752,7 +752,7 @@ async def test_zeroconf_flow( result["flow_id"], user_input=user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_NAME] == NAME @@ -781,7 +781,7 @@ async def test_zeroconf_flow_already_configured( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -808,7 +808,7 @@ async def test_zeroconf_flow_with_port_in_host( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -825,7 +825,7 @@ async def test_zeroconf_dupe_fail( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) @@ -834,7 +834,7 @@ async def test_zeroconf_dupe_fail( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -858,7 +858,7 @@ async def test_zeroconf_ignore( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_zeroconf_no_unique_id( @@ -873,7 +873,7 @@ async def test_zeroconf_no_unique_id( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -898,7 +898,7 @@ async def test_zeroconf_abort_when_ignored( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -927,7 +927,7 @@ async def test_zeroconf_flow_already_configured_hostname( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -952,7 +952,7 @@ async def test_import_flow_already_configured_hostname( ) # Flow should abort because device was updated - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" assert entry.data[CONF_HOST] == HOST diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index f86644b3447..66ca5c2cb20 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.vlc_telnet.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -56,7 +52,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( @@ -73,7 +69,7 @@ async def test_user_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == entry_data["host"] assert result["data"] == entry_data assert len(mock_setup_entry.mock_calls) == 1 @@ -98,7 +94,7 @@ async def test_abort_already_configured(hass: HomeAssistant, source: str) -> Non data=entry_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -137,7 +133,7 @@ async def test_errors( {"password": "test-password"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -177,7 +173,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert dict(entry.data) == {**entry_data, "password": "new-password"} @@ -232,7 +228,7 @@ async def test_reauth_errors( {"password": "test-password"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -263,11 +259,11 @@ async def test_hassio_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == test_data.config["name"] assert result2["data"] == test_data.config assert len(mock_setup_entry.mock_calls) == 1 @@ -294,7 +290,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT @pytest.mark.parametrize( @@ -336,9 +332,9 @@ async def test_hassio_errors( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == error diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py index 20f030bb99f..c45ab430c2e 100644 --- a/tests/components/vulcan/test_config_flow.py +++ b/tests/components/vulcan/test_config_flow.py @@ -36,7 +36,7 @@ async def test_show_form(hass): result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -56,7 +56,7 @@ async def test_config_flow_auth_success( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -70,7 +70,7 @@ async def test_config_flow_auth_success( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -93,7 +93,7 @@ async def test_config_flow_auth_success_with_multiple_students( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -102,7 +102,7 @@ async def test_config_flow_auth_success_with_multiple_students( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -115,7 +115,7 @@ async def test_config_flow_auth_success_with_multiple_students( {"student": "0"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_config_flow_reauth_success( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -154,7 +154,7 @@ async def test_config_flow_reauth_success( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -180,7 +180,7 @@ async def test_config_flow_reauth_without_matching_entries( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -189,7 +189,7 @@ async def test_config_flow_reauth_without_matching_entries( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_matching_entries" @@ -202,7 +202,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} with patch( @@ -215,7 +215,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_token"} @@ -229,7 +229,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "expired_token"} @@ -243,7 +243,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_pin"} @@ -257,7 +257,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_symbol"} @@ -271,7 +271,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -285,7 +285,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "unknown"} @@ -310,7 +310,7 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -319,7 +319,7 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student {"use_saved_credentials": False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -332,7 +332,7 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -353,7 +353,7 @@ async def test_multiple_config_entries_using_saved_credentials(mock_student, has const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -366,7 +366,7 @@ async def test_multiple_config_entries_using_saved_credentials(mock_student, has {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -387,7 +387,7 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -396,7 +396,7 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -409,7 +409,7 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h {"student": "0"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -438,7 +438,7 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -447,7 +447,7 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -460,7 +460,7 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -489,7 +489,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -498,7 +498,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -507,7 +507,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -520,7 +520,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h {"student": "0"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -545,7 +545,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -557,7 +557,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=UnauthorizedCertificateException, ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -566,7 +566,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_credentials"} @@ -593,7 +593,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -605,7 +605,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=ClientConnectionError, ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -614,7 +614,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] == {"base": "cannot_connect"} @@ -639,7 +639,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -651,7 +651,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=Exception, ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -660,7 +660,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} @@ -688,7 +688,7 @@ async def test_student_already_exists(mock_account, mock_keystore, mock_student, const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -697,7 +697,7 @@ async def test_student_already_exists(mock_account, mock_keystore, mock_student, {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "all_student_already_configured" @@ -713,7 +713,7 @@ async def test_config_flow_auth_invalid_token(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -722,7 +722,7 @@ async def test_config_flow_auth_invalid_token(mock_keystore, hass): {CONF_TOKEN: "3S20000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_token"} @@ -739,7 +739,7 @@ async def test_config_flow_auth_invalid_region(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -748,7 +748,7 @@ async def test_config_flow_auth_invalid_region(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_symbol"} @@ -765,7 +765,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -774,7 +774,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_pin"} @@ -791,7 +791,7 @@ async def test_config_flow_auth_expired_token(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -800,7 +800,7 @@ async def test_config_flow_auth_expired_token(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_token"} @@ -817,7 +817,7 @@ async def test_config_flow_auth_connection_error(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -826,7 +826,7 @@ async def test_config_flow_auth_connection_error(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "cannot_connect"} @@ -843,7 +843,7 @@ async def test_config_flow_auth_unknown_error(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -852,6 +852,6 @@ async def test_config_flow_auth_unknown_error(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 68f70878592..c9254f77aac 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -45,7 +45,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: flow.hass = hass result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index 514376d58a5..5e97bd2a396 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -22,11 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType @pytest.mark.parametrize( @@ -43,7 +39,7 @@ async def test_auth_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config_auth ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} @@ -78,7 +74,7 @@ async def test_coordinate_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == errors @@ -95,7 +91,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -106,13 +102,13 @@ async def test_options_flow(hass: HomeAssistant, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} @@ -130,7 +126,7 @@ async def test_show_form_coordinates( result["flow_id"], user_input=config_location_type ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "coordinates" assert result["errors"] is None @@ -140,7 +136,7 @@ async def test_show_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -166,7 +162,7 @@ async def test_step_reauth( user_input={CONF_PASSWORD: "password"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -188,7 +184,7 @@ async def test_step_user_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", @@ -213,7 +209,7 @@ async def test_step_user_home( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index c4414cdbefd..c4b8144b74d 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -29,7 +29,7 @@ async def test_minimum_fields(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -38,7 +38,7 @@ async def test_minimum_fields(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -60,7 +60,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -76,7 +76,7 @@ async def test_options(hass): CONF_VEHICLE_TYPE: "taxi", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_AVOID_FERRIES: True, @@ -122,7 +122,7 @@ async def test_import(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { @@ -148,7 +148,7 @@ async def test_dupe(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -157,13 +157,13 @@ async def test_dupe(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -172,7 +172,7 @@ async def test_dupe(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @pytest.mark.usefixtures("invalidate_config_entry") @@ -181,12 +181,12 @@ async def test_invalid_config_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 2deac022b1e..e8d88845f5a 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -23,7 +23,7 @@ async def test_webhook_json(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -62,7 +62,7 @@ async def test_webhook_post(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -97,7 +97,7 @@ async def test_webhook_query(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -126,13 +126,68 @@ async def test_webhook_query(hass, hass_client_no_auth): assert events[0].data["hello"] == "yo world" +async def test_webhook_multiple(hass, hass_client_no_auth): + """Test triggering multiple triggers with a POST webhook.""" + events1 = [] + events2 = [] + + @callback + def store_event1(event): + """Help store events.""" + events1.append(event) + + @callback + def store_event2(event): + """Help store events.""" + events2.append(event) + + hass.bus.async_listen("test_success1", store_event1) + hass.bus.async_listen("test_success2", store_event2) + + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success1", + "event_data_template": {"hello": "yo {{ trigger.data.hello }}"}, + }, + }, + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success2", + "event_data_template": { + "hello": "yo2 {{ trigger.data.hello }}" + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + client = await hass_client_no_auth() + + await client.post("/api/webhook/post_webhook", data={"hello": "world"}) + await hass.async_block_till_done() + + assert len(events1) == 1 + assert events1[0].data["hello"] == "yo world" + assert len(events2) == 1 + assert events2[0].data["hello"] == "yo2 world" + + async def test_webhook_reload(hass, hass_client_no_auth): """Test reloading a webhook.""" events = [] @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index 012682615dc..b5ad3f4cc2b 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -17,11 +17,7 @@ from homeassistant.const import ( CONF_SOURCE, CONF_UNIQUE_ID, ) -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import setup_webostv from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_APPS, MOCK_INPUTS, TV_NAME @@ -55,7 +51,7 @@ async def test_form(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -65,7 +61,7 @@ async def test_form(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_init( @@ -75,7 +71,7 @@ async def test_form(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" with patch("homeassistant.components.webostv.async_setup_entry", return_value=True): @@ -85,7 +81,7 @@ async def test_form(hass, client): await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == TV_NAME @@ -115,7 +111,7 @@ async def test_options_flow_live_tv_in_apps(hass, client, apps, inputs): result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -124,7 +120,7 @@ async def test_options_flow_live_tv_in_apps(hass, client, apps, inputs): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"] @@ -136,7 +132,7 @@ async def test_options_flow_cannot_retrieve(hass, client): result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve"} @@ -154,7 +150,7 @@ async def test_form_cannot_connect(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -172,7 +168,7 @@ async def test_form_pairexception(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "error_pairing" @@ -187,7 +183,7 @@ async def test_entry_already_configured(hass, client): data=MOCK_YAML_CONFIG, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -201,7 +197,7 @@ async def test_form_ssdp(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" @@ -216,7 +212,7 @@ async def test_ssdp_in_progress(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" result2 = await hass.config_entries.flow.async_init( @@ -224,7 +220,7 @@ async def test_ssdp_in_progress(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -239,7 +235,7 @@ async def test_ssdp_update_uuid(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:] @@ -258,7 +254,7 @@ async def test_ssdp_not_update_uuid(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "pairing" assert entry.unique_id is None @@ -276,7 +272,7 @@ async def test_form_abort_uuid_configured(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_config = { @@ -291,7 +287,7 @@ async def test_form_abort_uuid_configured(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -300,6 +296,6 @@ async def test_form_abort_uuid_configured(hass, client): await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "new_host" diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 0f4695596fc..f1065061c73 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -22,7 +22,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.loader import async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_setup_component -from tests.common import MockEntity, MockEntityPlatform, async_mock_service +from tests.common import ( + MockEntity, + MockEntityPlatform, + MockModule, + async_mock_service, + mock_integration, +) STATE_KEY_SHORT_NAMES = { "entity_id": "e", @@ -1749,3 +1755,36 @@ async def test_validate_config_invalid(websocket_client, key, config, error): assert msg["type"] == const.TYPE_RESULT assert msg["success"] assert msg["result"] == {key: {"valid": False, "error": error}} + + +async def test_supported_brands(hass, websocket_client): + """Test supported brands.""" + mock_integration( + hass, + MockModule("test", partial_manifest={"supported_brands": {"hello": "World"}}), + ) + mock_integration( + hass, + MockModule( + "abcd", partial_manifest={"supported_brands": {"something": "Something"}} + ), + ) + + with patch( + "homeassistant.generated.supported_brands.HAS_SUPPORTED_BRANDS", + ("abcd", "test"), + ): + await websocket_client.send_json({"id": 7, "type": "supported_brands"}) + msg = await websocket_client.receive_json() + + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == { + "abcd": { + "something": "Something", + }, + "test": { + "hello": "World", + }, + } diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 3a54d5912e0..fd9af99c1a4 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -7,30 +7,10 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.components import websocket_api -from homeassistant.components.websocket_api import const from tests.common import MockUser -async def test_send_big_result(hass, websocket_client): - """Test sending big results over the WS.""" - - @websocket_api.websocket_command({"type": "big_result"}) - @websocket_api.async_response - async def send_big_result(hass, connection, msg): - await connection.send_big_result(msg["id"], {"big": "result"}) - - websocket_api.async_register_command(hass, send_big_result) - - await websocket_client.send_json({"id": 5, "type": "big_result"}) - - msg = await websocket_client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert msg["success"] - assert msg["result"] == {"big": "result"} - - async def test_exception_handling(): """Test handling of exceptions.""" send_messages = [] diff --git a/tests/components/wemo/test_config_flow.py b/tests/components/wemo/test_config_flow.py index 81040e44c9c..0ffcb9d7f5e 100644 --- a/tests/components/wemo/test_config_flow.py +++ b/tests/components/wemo/test_config_flow.py @@ -18,5 +18,5 @@ async def test_not_discovered(hass: HomeAssistant) -> None: with patch("homeassistant.components.wemo.config_flow.pywemo") as mock_pywemo: mock_pywemo.discover_devices.return_value = [] result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/whois/test_config_flow.py b/tests/components/whois/test_config_flow.py index 4bf6d7e8731..7250a9d1567 100644 --- a/tests/components/whois/test_config_flow.py +++ b/tests/components/whois/test_config_flow.py @@ -13,11 +13,7 @@ from homeassistant.components.whois.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -32,7 +28,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -41,7 +37,7 @@ async def test_full_user_flow( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Example.com" assert result2.get("data") == {CONF_DOMAIN: "example.com"} @@ -73,7 +69,7 @@ async def test_full_flow_with_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -83,7 +79,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": reason} assert "flow_id" in result2 @@ -97,7 +93,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Example.com" assert result3.get("data") == {CONF_DOMAIN: "example.com"} @@ -120,7 +116,7 @@ async def test_already_configured( data={CONF_DOMAIN: "HOME-Assistant.io"}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/whois/test_sensor.py b/tests/components/whois/test_sensor.py index 89b84dc5849..828a9242621 100644 --- a/tests/components/whois/test_sensor.py +++ b/tests/components/whois/test_sensor.py @@ -60,7 +60,7 @@ async def test_whois_sensors( assert state.state == "364" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "home-assistant.io Days Until Expiration" + == "home-assistant.io Days until expiration" ) assert state.attributes.get(ATTR_ICON) == "mdi:calendar-clock" assert ATTR_DEVICE_CLASS not in state.attributes @@ -83,7 +83,7 @@ async def test_whois_sensors( assert entry.unique_id == "home-assistant.io_last_updated" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "2021-12-31T23:00:00+00:00" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last Updated" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last updated" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes @@ -137,9 +137,9 @@ async def test_whois_sensors( assert device_entry.configuration_url is None assert device_entry.entry_type == dr.DeviceEntryType.SERVICE assert device_entry.identifiers == {(DOMAIN, "home-assistant.io")} + assert device_entry.name == "home-assistant.io" assert device_entry.manufacturer is None assert device_entry.model is None - assert device_entry.name is None assert device_entry.sw_version is None @@ -159,7 +159,7 @@ async def test_whois_sensors_missing_some_attrs( assert entry.unique_id == "home-assistant.io_last_updated" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "2021-12-31T23:00:00+00:00" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last Updated" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last updated" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index 50433d377a9..b48641d7ea1 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -7,11 +7,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.wiffi.const import DOMAIN from homeassistant.const import CONF_PORT, CONF_TIMEOUT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -77,7 +73,7 @@ async def test_form(hass, dummy_tcp_server): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER @@ -85,7 +81,7 @@ async def test_form(hass, dummy_tcp_server): result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY async def test_form_addr_in_use(hass, addr_in_use): @@ -98,7 +94,7 @@ async def test_form_addr_in_use(hass, addr_in_use): result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "addr_in_use" @@ -112,7 +108,7 @@ async def test_form_start_server_failed(hass, start_server_failed): result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "start_server_failed" @@ -125,13 +121,13 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMEOUT: 9} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMEOUT] == 9 diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index 326224b02c9..a209d55ba99 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -12,11 +12,7 @@ from homeassistant.components.wilight.config_flow import ( from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.wilight import ( @@ -61,7 +57,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -77,7 +73,7 @@ async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -89,7 +85,7 @@ async def test_ssdp_not_wilight_abort_2(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -103,7 +99,7 @@ async def test_ssdp_not_wilight_abort_3( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -117,7 +113,7 @@ async def test_ssdp_not_supported_abort( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_supported_device" @@ -142,7 +138,7 @@ async def test_ssdp_device_exists_abort(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -154,7 +150,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -165,7 +161,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"WL{WILIGHT_ID}" assert result["data"] diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b90a004ed0b..f3855ae96f0 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -42,6 +42,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import AUTH_CALLBACK_PATH from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -167,6 +168,10 @@ class ComponentFactory: ) api_mock: ConfigEntryWithingsApi = MagicMock(spec=ConfigEntryWithingsApi) + api_mock.config_entry = MockConfigEntry( + domain=const.DOMAIN, + data={"profile": profile_config.profile}, + ) ComponentFactory._setup_api_method( api_mock.user_get_device, profile_config.api_response_user_get_device ) @@ -199,7 +204,7 @@ class ComponentFactory: "redirect_uri": "http://127.0.0.1:8080/auth/external/callback", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={self._client_id}&" @@ -301,15 +306,6 @@ def get_config_entries_for_user_id( ) -def async_get_flow_for_user_id(hass: HomeAssistant, user_id: int) -> list[dict]: - """Get a flow for a user id.""" - return [ - flow - for flow in hass.config_entries.flow.async_progress() - if flow["handler"] == const.DOMAIN and flow["context"].get("userid") == user_id - ] - - def get_data_manager_by_user_id( hass: HomeAssistant, user_id: int ) -> DataManager | None: diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 2643ac18c24..9fcc84dbe83 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -62,11 +62,17 @@ async def test_config_reauth_profile( result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": config_entries.SOURCE_REAUTH, "profile": "person0"}, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "title_placeholders": {"name": config_entry.title}, + "unique_id": config_entry.unique_id, + }, + data={"profile": "person0"}, ) assert result assert result["type"] == "form" - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {const.PROFILE: "person0"} result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index db26f7328ce..83e8a622e78 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -20,12 +20,7 @@ from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_setup_component -from .common import ( - ComponentFactory, - async_get_flow_for_user_id, - get_data_manager_by_user_id, - new_profile_config, -) +from .common import ComponentFactory, get_data_manager_by_user_id, new_profile_config from tests.common import MockConfigEntry @@ -122,6 +117,7 @@ async def test_async_setup_no_config(hass: HomeAssistant) -> None: [Exception("401, this is the message")], ], ) +@patch("homeassistant.components.withings.common._RETRY_COEFFICIENT", 0) async def test_auth_failure( hass: HomeAssistant, component_factory: ComponentFactory, @@ -138,20 +134,18 @@ async def test_auth_failure( ) await component_factory.configure_component(profile_configs=(person0,)) - assert not async_get_flow_for_user_id(hass, person0.user_id) + assert not hass.config_entries.flow.async_progress() await component_factory.setup_profile(person0.user_id) data_manager = get_data_manager_by_user_id(hass, person0.user_id) await data_manager.poll_data_update_coordinator.async_refresh() - flows = async_get_flow_for_user_id(hass, person0.user_id) + flows = hass.config_entries.flow.async_progress() assert flows assert len(flows) == 1 flow = flows[0] assert flow["handler"] == const.DOMAIN - assert flow["context"]["profile"] == person0.profile - assert flow["context"]["userid"] == person0.user_id result = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={} diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index f37f2ba21a0..75ab1d1b188 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant.components import dhcp from homeassistant.components.wiz.config_flow import CONF_DEVICE from homeassistant.components.wiz.const import DOMAIN from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( FAKE_DIMMABLE_BULB, @@ -85,7 +85,7 @@ async def test_user_flow_enters_dns_name(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "no_ip"} with _patch_wizlight(), patch( @@ -179,7 +179,7 @@ async def test_discovered_by_dhcp_connection_fails(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -256,7 +256,7 @@ async def test_discovered_by_dhcp_or_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with _patch_wizlight( @@ -306,7 +306,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_updates_host( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == FAKE_IP @@ -335,7 +335,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_avoid_waiting_for_ret ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.state is config_entries.ConfigEntryState.LOADED @@ -458,7 +458,7 @@ async def test_setup_via_discovery_exception_finds_nothing(hass): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -476,7 +476,7 @@ async def test_discovery_with_firmware_update(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" # In between discovery and when the user clicks to set it up the firmware diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index e1cf08069da..600bf0eb0d2 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.wled.const import CONF_KEEP_MASTER_LIGHT, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result = await hass.config_entries.flow.async_configure( @@ -35,7 +31,7 @@ async def test_full_user_flow_implementation( ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -68,7 +64,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result.get("description_placeholders") == {CONF_NAME: "WLED RGB Light"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +72,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result2.get("title") == "WLED RGB Light" - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -106,7 +102,7 @@ async def test_zeroconf_during_onboarding( ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_HOST: "192.168.1.123"} assert "result" in result @@ -127,7 +123,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -152,7 +148,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -169,7 +165,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -186,7 +182,7 @@ async def test_user_with_cct_channel_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -211,7 +207,7 @@ async def test_zeroconf_without_mac_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -236,7 +232,7 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -261,7 +257,7 @@ async def test_zeroconf_with_cct_channel_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -273,7 +269,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -282,7 +278,7 @@ async def test_options_flow( user_input={CONF_KEEP_MASTER_LIGHT: True}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_KEEP_MASTER_LIGHT: True, } diff --git a/tests/components/wled/test_diagnostics.py b/tests/components/wled/test_diagnostics.py index d8782848c92..8f086331f8f 100644 --- a/tests/components/wled/test_diagnostics.py +++ b/tests/components/wled/test_diagnostics.py @@ -26,7 +26,9 @@ async def test_diagnostics( "free_heap": 14600, "leds": { "__type": "", - "repr": "Leds(cct=False, count=30, fps=None, max_power=850, max_segments=10, power=470, rgbw=False, wv=True)", + "repr": "Leds(cct=False, count=30, fps=None, light_capabilities=None, " + "max_power=850, max_segments=10, power=470, rgbw=False, wv=True, " + "segment_light_capabilities=None)", }, "live_ip": "Unknown", "live_mode": "Unknown", diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index f0530524805..9a6105a4b8b 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -38,7 +38,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -52,7 +52,7 @@ async def test_device_step_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device" @@ -71,7 +71,7 @@ async def test_create_entry(hass): {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result_create_entry["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result_create_entry["title"] == CONFIG[DEVICE_NAME] assert result_create_entry["data"] == CONFIG @@ -138,5 +138,5 @@ async def test_already_configured_error(hass): {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_create_entry["type"] == data_entry_flow.FlowResultType.ABORT assert result_create_entry["reason"] == "already_configured" diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index 4fe3554941d..da5a16882e8 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -126,7 +126,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -141,7 +141,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == { "1": "one", "2": "too", diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index f8c296dcbbe..2ce497bcbc1 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -19,7 +19,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( "xbox", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py new file mode 100644 index 00000000000..1dd1eeed65a --- /dev/null +++ b/tests/components/xiaomi_ble/__init__.py @@ -0,0 +1,89 @@ +"""Tests for the SensorPush integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="00:00:00:00:00:00", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfo( + name="LYWSDCGQ", + address="58:2D:34:35:93:21", + rssi=-63, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"P \xaa\x01\xda!\x9354-X\r\x10\x04\xfe\x00H\x02" + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + +MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( + name="MMC_T201_1", + address="00:81:F9:DD:6F:C1", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b'p"\xdb\x00o\xc1o\xdd\xf9\x81\x00\t\x00 \x05\xc6\rc\rQ' + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + +JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfo( + name="JTYJGD03MI", + address="54:EF:44:E3:9C:BC", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b'XY\x97\td\xbc\x9c\xe3D\xefT" `\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3' + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + +YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( + name="YLKG07YL", + address="F8:24:41:C5:98:8B", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + +MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfo( + name="LYWSD02MMC", + address="A4:C1:38:56:53:84", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"0X[\x05\x02\x84\x53\x568\xc1\xa4\x08", + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + + +def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: + """Make a dummy advertisement.""" + return BluetoothServiceInfo( + name="Test Device", + address=address, + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": payload, + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", + ) diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py new file mode 100644 index 00000000000..9fce8e85ea8 --- /dev/null +++ b/tests/components/xiaomi_ble/conftest.py @@ -0,0 +1,8 @@ +"""Session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py new file mode 100644 index 00000000000..32ba6be3322 --- /dev/null +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -0,0 +1,1041 @@ +"""Test the Xiaomi config flow.""" + +import asyncio +from unittest.mock import patch + +from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData + +from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + JTYJGD03MI_SERVICE_INFO, + LYWSDCGQ_SERVICE_INFO, + MISSING_PAYLOAD_ENCRYPTED, + MMC_T201_1_SERVICE_INFO, + NOT_SENSOR_PUSH_SERVICE_INFO, + YLKG07YL_SERVICE_INFO, + make_advertisement, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "MMC_T201_1" + assert result2["data"] == {} + assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" + + +async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): + """Test discovery via bluetooth with a valid device but missing payload.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" + + +async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): + """Test discovering a valid device. Payload is too short, but later we get full one.""" + + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" + + +async def test_async_step_bluetooth_during_onboarding(hass): + """Test discovery via bluetooth during onboarding.""" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "MMC_T201_1" + assert result["data"] == {} + assert result["result"].unique_id == "00:81:F9:DD:6F:C1" + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + +async def test_async_step_bluetooth_valid_device_legacy_encryption(hass): + """Test discovery via bluetooth with a valid device, with legacy encryption.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YLKG07YL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key(hass): + """Test discovery via bluetooth with a valid device, with legacy encryption and invalid key.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YLKG07YL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_length( + hass, +): + """Test discovery via bluetooth with a valid device, with legacy encryption and wrong key length.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YLKG07YL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "expected_24_characters" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_bluetooth_valid_device_v4_encryption(hass): + """Test discovery via bluetooth with a valid device, with v4 encryption.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=JTYJGD03MI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key(hass): + """Test discovery via bluetooth with a valid device, with v4 encryption and wrong key.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=JTYJGD03MI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length(hass): + """Test discovery via bluetooth with a valid device, with v4 encryption and wrong key length.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=JTYJGD03MI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18fda143a58"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "expected_32_characters" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_bluetooth_not_xiaomi(hass): + """Test discovery via bluetooth not xiaomi.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_SENSOR_PUSH_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_no_devices_found_2(hass): + """ + Test setup from service info cache with no devices found. + + This variant tests with a non-Xiaomi device known to us. + """ + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[NOT_SENSOR_PUSH_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "58:2D:34:35:93:21"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSDCGQ" + assert result2["data"] == {} + assert result2["result"].unique_id == "58:2D:34:35:93:21" + + +async def test_async_step_user_short_payload(hass): + """Test setup from service info cache with devices found but short payloads.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "LYWSD02MMC" + assert result3["data"] == {} + assert result3["result"].unique_id == "A4:C1:38:56:53:84" + + +async def test_async_step_user_short_payload_then_full(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + + +async def test_async_step_user_with_found_devices_v4_encryption(hass): + """Test setup from service info cache with devices found, with v4 encryption.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[JTYJGD03MI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:EF:44:E3:9C:BC"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_user_with_found_devices_v4_encryption_wrong_key(hass): + """Test setup from service info cache with devices found, with v4 encryption and wrong key.""" + # Get a list of devices + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[JTYJGD03MI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Pick a device + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:EF:44:E3:9C:BC"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + # Try an incorrect key + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Check can still finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length(hass): + """Test setup from service info cache with devices found, with v4 encryption and wrong key length.""" + # Get a list of devices + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[JTYJGD03MI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Select a single device + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:EF:44:E3:9C:BC"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + # Try an incorrect key + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef1dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "expected_32_characters" + + # Check can still finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_user_with_found_devices_legacy_encryption(hass): + """Test setup from service info cache with devices found, with legacy encryption.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[YLKG07YL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F8:24:41:C5:98:8B"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_legacy" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( + hass, +): + """Test setup from service info cache with devices found, with legacy encryption and wrong key.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[YLKG07YL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F8:24:41:C5:98:8B"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_legacy" + + # Enter an incorrect code + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Check you can finish the flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_length( + hass, +): + """Test setup from service info cache with devices found, with legacy encryption and wrong key length.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[YLKG07YL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F8:24:41:C5:98:8B"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_legacy" + + # Enter an incorrect code + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b85307518487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "expected_24_characters" + + # Check you can finish the flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:35:93:21", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="00:81:F9:DD:6F:C1", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MMC_T201_1_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "00:81:F9:DD:6F:C1"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "MMC_T201_1" + assert result2["data"] == {} + assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) + + +async def test_async_step_reauth_legacy(hass): + """Test reauth with a legacy key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_legacy_wrong_key(hass): + """Test reauth with a bad legacy key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b85307515a487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4(hass): + """Test reauth with a v4 key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4_wrong_key(hass): + """Test reauth for v4 with a bad key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_abort_early(hass): + """ + Test we can abort the reauth if there is no encryption. + + (This can't currently happen in practice). + """ + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + + device = DeviceData() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + }, + data=entry.data | {"device": device}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py new file mode 100644 index 00000000000..011c6daecae --- /dev/null +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -0,0 +1,325 @@ +"""Test the Xiaomi config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import MMC_T201_1_SERVICE_INFO, make_advertisement + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="00:81:F9:DD:6F:C1", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(MMC_T201_1_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + temp_sensor = hass.states.get("sensor.mmc_t201_1_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "36.8719980616822" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "MMC_T201_1 Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_formaldeyhde(hass): + """Make sure that formldehyde sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1010, payload len is 0x2 and payload is 0xf400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x10\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("sensor.test_device_formaldehyde") + sensor_attr = sensor.attributes + assert sensor.state == "2.44" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Formaldehyde" + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "mg/m³" + assert sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_consumable(hass): + """Make sure that consumable sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x13\x10\x02\x60\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("sensor.test_device_consumable") + sensor_attr = sensor.attributes + assert sensor.state == "96" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Consumable" + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_battery_voltage(hass): + """Make sure that battery voltage sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x0a10, payload len is 0x2 and payload is 0x6400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x0a\x10\x02\x64\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + volt_sensor = hass.states.get("sensor.test_device_voltage") + volt_sensor_attr = volt_sensor.attributes + assert volt_sensor.state == "3.1" + assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Voltage" + assert volt_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "V" + assert volt_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + bat_sensor = hass.states.get("sensor.test_device_battery") + bat_sensor_attr = bat_sensor.attributes + assert bat_sensor.state == "100" + assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert bat_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert bat_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_HHCCJCY01(hass): + """This device has multiple advertisements before all sensors are visible. Test that this works.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00fz>j\x8d|\xc4\r\x07\x10\x03\x00\x00\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00hz>j\x8d|\xc4\r\t\x10\x02W\x02" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00Gz>j\x8d|\xc4\r\x08\x10\x01@" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x04\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + illum_sensor = hass.states.get("sensor.test_device_illuminance") + illum_sensor_attr = illum_sensor.attributes + assert illum_sensor.state == "0" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Illuminance" + assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + cond_sensor = hass.states.get("sensor.test_device_conductivity") + cond_sensor_attribtes = cond_sensor.attributes + assert cond_sensor.state == "599" + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Conductivity" + assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + moist_sensor = hass.states.get("sensor.test_device_moisture") + moist_sensor_attribtes = moist_sensor.attributes + assert moist_sensor.state == "64" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Moisture" + assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "24.4" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_CGDK2(hass): + """This device has encrypion so we need to retrieve its bindkey from the configentry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:12:20:89", + data={"bindkey": "a3bfe9853dd85a620debe3620caaa351"}, + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "58:2D:34:12:20:89", + b"XXo\x06\x07\x89 \x124-X_\x17m\xd5O\x02\x00\x00/\xa4S\xfa", + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "22.6" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 2aa27d703a1..e47a1a1ace5 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -872,7 +872,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -882,7 +882,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_CLOUD_SUBDEVICES: True, } @@ -912,7 +912,7 @@ async def test_options_flow_incomplete(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -921,7 +921,7 @@ async def test_options_flow_incomplete(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cloud_credentials_incomplete"} diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index 97a4cdfed6b..a53429967e6 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -9,11 +9,7 @@ from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError from homeassistant import config_entries from homeassistant.components.yale_smart_alarm.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -24,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -43,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -85,7 +81,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -104,7 +100,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -138,7 +134,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -156,7 +152,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -218,7 +214,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -237,7 +233,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -265,7 +261,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -273,7 +269,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={"code": "123456", "lock_code_digits": 6}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {"code": "123456", "lock_code_digits": 6} @@ -295,7 +291,7 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -304,7 +300,7 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: user_input={"code": "123", "lock_code_digits": 6}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "code_format_mismatch"} @@ -313,5 +309,5 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: user_input={"code": "123456", "lock_code_digits": 6}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {"code": "123456", "lock_code_digits": 6} diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index fc9a740d4a6..a3fb0cf6211 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -128,13 +128,13 @@ async def test_user_input_device_not_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "none"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -146,13 +146,13 @@ async def test_user_input_non_yamaha_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "no_musiccast_device"} @@ -176,7 +176,7 @@ async def test_user_input_device_already_existing( {"host": "192.168.188.18"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -188,13 +188,13 @@ async def test_user_input_unknown_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -209,13 +209,13 @@ async def test_user_input_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -235,13 +235,13 @@ async def test_user_input_device_found_no_ssdp( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -269,7 +269,7 @@ async def test_ssdp_discovery_failed(hass, mock_ssdp_no_yamaha, mock_get_source_ ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "yxc_control_url_missing" @@ -291,7 +291,7 @@ async def test_ssdp_discovery_successful_add_device( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "confirm" @@ -300,7 +300,7 @@ async def test_ssdp_discovery_successful_add_device( {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -332,7 +332,7 @@ async def test_ssdp_discovery_existing_device_update( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data[CONF_HOST] == "127.0.0.1" assert mock_entry.data["upnp_description"] == "http://127.0.0.1/desc.xml" diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 7d9acc670b5..391c969d893 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -23,7 +23,7 @@ from homeassistant.components.yeelight.const import ( ) from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( CAPABILITIES, @@ -475,7 +475,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -489,7 +489,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_discovery_interval(), patch( @@ -503,7 +503,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with _patch_discovery( @@ -519,7 +519,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -558,7 +558,7 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -587,7 +587,7 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -626,7 +626,7 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -642,7 +642,7 @@ async def test_discovered_ssdp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -671,7 +671,7 @@ async def test_discovered_ssdp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -689,7 +689,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -720,7 +720,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_bulb = _mocked_bulb() @@ -734,7 +734,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -756,7 +756,7 @@ async def test_discovery_updates_ip(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -784,7 +784,7 @@ async def test_discovery_updates_ip_no_reload_setup_in_progress(hass: HomeAssist ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS assert len(mock_setup_entry.mock_calls) == 0 @@ -806,7 +806,7 @@ async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index e224bc3e1d2..f809596e816 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -22,7 +22,7 @@ async def test_abort_if_no_configuration(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -32,7 +32,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -60,7 +60,7 @@ async def test_full_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -126,7 +126,7 @@ async def test_abort_if_authorization_timeout(hass, current_request_with_host): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -203,6 +203,6 @@ async def test_reauthentication( assert token_data["refresh_token"] == "mock-refresh-token" assert token_data["type"] == "Bearer" assert token_data["expires_in"] == 60 - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/youless/test_config_flows.py b/tests/components/youless/test_config_flows.py index d7d9a39ec6e..5bf119681c4 100644 --- a/tests/components/youless/test_config_flows.py +++ b/tests/components/youless/test_config_flows.py @@ -5,7 +5,7 @@ from urllib.error import URLError from homeassistant.components.youless import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType def _get_mock_youless_api(initialize=None): @@ -25,7 +25,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -42,7 +42,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "localhost" assert len(mocked_youless.mock_calls) == 1 @@ -53,7 +53,7 @@ async def test_not_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -68,5 +68,5 @@ async def test_not_found(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert len(mocked_youless.mock_calls) == 1 diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 88dceb9d464..6bc37e10da2 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -530,7 +530,8 @@ async def test_homekit_match_partial_space(hass, mock_async_zeroconf): await hass.async_block_till_done() assert len(mock_service_browser.mock_calls) == 1 - assert len(mock_config_flow.mock_calls) == 1 + # One for HKC, and one for LIFX since lifx is local polling + assert len(mock_config_flow.mock_calls) == 2 assert mock_config_flow.mock_calls[0][1][0] == "lifx" @@ -741,6 +742,44 @@ async def test_homekit_controller_still_discovered_unpaired_for_cloud( assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" +async def test_homekit_controller_still_discovered_unpaired_for_polling( + hass, mock_async_zeroconf +): + """Test discovery is still passed to homekit controller when unpaired and discovered by polling integration. + + Since we prefer local push, if the integration that is being discovered + is polling AND the homekit device is unpaired we still want to discovery it + """ + with patch.dict( + zc_gen.ZEROCONF, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), patch.dict( + zc_gen.HOMEKIT, + {"iSmartGate": "gogogate2"}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, + "HaAsyncServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._udp.local." + ), + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_homekit_info_mock("iSmartGate", HOMEKIT_STATUS_UNPAIRED), + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[0][1][0] == "gogogate2" + assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" + + async def test_info_from_service_non_utf8(hass): """Test info_from_service handles non UTF-8 property keys and values correctly.""" service_type = "_test._tcp.local." diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 757587071fd..cad8020267f 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,5 +1,6 @@ """Common test objects.""" import asyncio +from datetime import timedelta import math from unittest.mock import AsyncMock, Mock @@ -7,7 +8,10 @@ import zigpy.zcl import zigpy.zcl.foundation as zcl_f import homeassistant.components.zha.core.const as zha_const -from homeassistant.util import slugify +from homeassistant.helpers import entity_registry +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed def patch_cluster(cluster): @@ -133,7 +137,7 @@ async def find_entity_id(domain, zha_device, hass, qualifier=None): This is used to get the entity id in order to get the state from the state machine so that we can test state changes. """ - entities = await find_entity_ids(domain, zha_device, hass) + entities = find_entity_ids(domain, zha_device, hass) if not entities: return None if qualifier: @@ -144,28 +148,26 @@ async def find_entity_id(domain, zha_device, hass, qualifier=None): return entities[0] -async def find_entity_ids(domain, zha_device, hass): +def find_entity_ids(domain, zha_device, hass): """Find the entity ids under the testing. This is used to get the entity id in order to get the state from the state machine so that we can test state changes. """ - ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) - head = f"{domain}.{slugify(f'{zha_device.name} {ieeetail}')}" - enitiy_ids = hass.states.async_entity_ids(domain) - await hass.async_block_till_done() - - res = [] - for entity_id in enitiy_ids: - if entity_id.startswith(head): - res.append(entity_id) - return res + registry = entity_registry.async_get(hass) + return [ + entity.entity_id + for entity in entity_registry.async_entries_for_device( + registry, zha_device.device_id + ) + if entity.domain == domain + ] def async_find_group_entity_id(hass, domain, group): """Find the group entity id under test.""" - entity_id = f"{domain}.{group.name.lower().replace(' ','_')}_zha_group_0x{group.group_id:04x}" + entity_id = f"{domain}.fakemanufacturer_fakemodel_{group.name.lower().replace(' ','_')}_zha_group_0x{group.group_id:04x}" entity_ids = hass.states.async_entity_ids(domain) @@ -234,3 +236,10 @@ async def async_wait_for_updates(hass): await asyncio.sleep(0) await asyncio.sleep(0) await hass.async_block_till_done() + + +async def async_shift_time(hass): + """Shift time to cause call later tasks to run.""" + next_update = dt_util.utcnow() + timedelta(seconds=11) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 482a11b95de..b1041a3e2a3 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -15,7 +15,6 @@ from zigpy.state import State import zigpy.types import zigpy.zdo.types as zdo_t -from homeassistant.components.zha import DOMAIN import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.device as zha_core_device from homeassistant.setup import async_setup_component @@ -71,11 +70,14 @@ async def config_entry_fixture(hass): }, options={ zha_const.CUSTOM_CONFIGURATION: { + zha_const.ZHA_OPTIONS: { + zha_const.CONF_ENABLE_ENHANCED_LIGHT_TRANSITION: True, + }, zha_const.ZHA_ALARM_OPTIONS: { zha_const.CONF_ALARM_ARM_REQUIRES_CODE: False, zha_const.CONF_ALARM_MASTER_CODE: "4321", zha_const.CONF_ALARM_FAILED_TRIES: 2, - } + }, } }, ) @@ -188,26 +190,14 @@ def zha_device_joined(hass, setup_zha): @pytest.fixture -def zha_device_restored(hass, zigpy_app_controller, setup_zha, hass_storage): +def zha_device_restored(hass, zigpy_app_controller, setup_zha): """Return a restored ZHA device.""" async def _zha_device(zigpy_dev, last_seen=None): zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev if last_seen is not None: - hass_storage[f"{DOMAIN}.storage"] = { - "key": f"{DOMAIN}.storage", - "version": 1, - "data": { - "devices": [ - { - "ieee": str(zigpy_dev.ieee), - "last_seen": last_seen, - "name": f"{zigpy_dev.manufacturer} {zigpy_dev.model}", - } - ], - }, - } + zigpy_dev.last_seen = last_seen await setup_zha() zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 7701992cab4..6aba5500a2a 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -151,7 +151,18 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): }, ), (0x0202, 1, {"fan_mode"}), - (0x0300, 1, {"current_x", "current_y", "color_temperature"}), + ( + 0x0300, + 1, + { + "current_x", + "current_y", + "color_temperature", + "current_hue", + "enhanced_current_hue", + "current_saturation", + }, + ), (0x0400, 1, {"measured_value"}), (0x0401, 1, {"level_status"}), (0x0402, 1, {"measured_value"}), diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 285c08d8a3e..84290595f12 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -25,11 +25,7 @@ from homeassistant.config_entries import ( SOURCE_ZEROCONF, ) from homeassistant.const import CONF_SOURCE -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -72,7 +68,7 @@ async def test_discovery(detect_mock, hass): flow["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "socket://192.168.1.200:6638" assert result["data"] == { CONF_DEVICE: { @@ -104,7 +100,7 @@ async def test_zigate_via_zeroconf(probe_mock, hass): flow["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "socket://192.168.1.200:1234" assert result["data"] == { CONF_DEVICE: { @@ -134,7 +130,7 @@ async def test_efr32_via_zeroconf(probe_mock, hass): flow["flow_id"], user_input={"baudrate": 115200} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "socket://192.168.1.200:6638" assert result["data"] == { CONF_DEVICE: { @@ -176,7 +172,7 @@ async def test_discovery_via_zeroconf_ip_change(detect_mock, hass): "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "socket://192.168.1.22:6638", @@ -209,7 +205,7 @@ async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass): "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "socket://192.168.1.22:6638", @@ -231,7 +227,7 @@ async def test_discovery_via_usb(detect_mock, hass): "zha", context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): @@ -240,7 +236,7 @@ async def test_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert "zigbee radio" in result2["title"] assert result2["data"] == { "device": { @@ -267,7 +263,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): "zha", context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): @@ -276,7 +272,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert ( "zigate radio - /dev/ttyZIGBEE, s/n: 1234 - test - 6015:0403" in result2["title"] @@ -304,7 +300,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): "zha", context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): @@ -313,7 +309,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "usb_probe_failed" @@ -338,7 +334,7 @@ async def test_discovery_via_usb_already_setup(detect_mock, hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -372,7 +368,7 @@ async def test_discovery_via_usb_path_changes(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyZIGBEE", @@ -457,7 +453,7 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" @@ -485,7 +481,7 @@ async def test_discovery_via_usb_zha_ignored_updates(detect_mock, hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyZIGBEE", @@ -535,7 +531,7 @@ async def test_user_flow(detect_mock, hass): context={CONF_SOURCE: SOURCE_USER}, data={zigpy.config.CONF_DEVICE_PATH: port_select}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"].startswith(port.description) assert result["data"] == {CONF_RADIO_TYPE: "test_radio"} assert detect_mock.await_count == 1 @@ -559,7 +555,7 @@ async def test_user_flow_not_detected(detect_mock, hass): data={zigpy.config.CONF_DEVICE_PATH: port_select}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_radio" assert detect_mock.await_count == 1 assert detect_mock.await_args[0][0] == port.device @@ -573,7 +569,7 @@ async def test_user_flow_show_form(hass): context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -585,7 +581,7 @@ async def test_user_flow_show_manual(hass): context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_radio" @@ -597,7 +593,7 @@ async def test_user_flow_manual(hass): context={CONF_SOURCE: SOURCE_USER}, data={zigpy.config.CONF_DEVICE_PATH: config_flow.CONF_MANUAL_PATH}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_radio" @@ -608,7 +604,7 @@ async def test_pick_radio_flow(hass, radio_type): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: "pick_radio"}, data={CONF_RADIO_TYPE: radio_type} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "port_config" @@ -708,7 +704,7 @@ async def test_user_port_config_fail(probe_mock, hass): result["flow_id"], user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "port_config" assert result["errors"]["base"] == "cannot_connect" assert probe_mock.await_count == 1 @@ -793,7 +789,7 @@ async def test_hardware_not_onboarded(hass): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyAMA1" assert result["data"] == { CONF_DEVICE: { @@ -823,14 +819,14 @@ async def test_hardware_onboarded(hass): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm_hardware" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyAMA1" assert result["data"] == { CONF_DEVICE: { @@ -861,7 +857,7 @@ async def test_hardware_already_setup(hass): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -875,5 +871,5 @@ async def test_hardware_invalid_data(hass, data): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_hardware_data" diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 3dab405151d..0f55735ecb2 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -345,7 +345,7 @@ async def test_restore_state(hass, zha_device_restored, zigpy_shade_device): hass, ( State( - "cover.fakemanufacturer_fakemodel_e769900a_level_on_off_shade", + "cover.fakemanufacturer_fakemodel_shade", STATE_OPEN, {ATTR_CURRENT_POSITION: 50}, ), diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 8b718635b6a..733a8e99e4b 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -6,7 +6,9 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha +import zigpy.types import zigpy.zcl.clusters.general as general +import zigpy.zdo.types as zdo_t from homeassistant.components.zha.core.const import ( CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, @@ -42,7 +44,7 @@ def required_platforms_only(): def zigpy_device(zigpy_device_mock): """Device tracker zigpy device.""" - def _dev(with_basic_channel: bool = True): + def _dev(with_basic_channel: bool = True, **kwargs): in_clusters = [general.OnOff.cluster_id] if with_basic_channel: in_clusters.append(general.Basic.cluster_id) @@ -54,7 +56,7 @@ def zigpy_device(zigpy_device_mock): SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, } } - return zigpy_device_mock(endpoints) + return zigpy_device_mock(endpoints, **kwargs) return _dev @@ -311,7 +313,7 @@ async def test_device_restore_availability( zha_device = await zha_device_restored( zigpy_device, last_seen=time.time() - last_seen_delta ) - entity_id = "switch.fakemanufacturer_fakemodel_e769900a_on_off" + entity_id = "switch.fakemanufacturer_fakemodel_switch" await hass.async_block_till_done() # ensure the switch entity was created @@ -321,3 +323,31 @@ async def test_device_restore_availability( assert hass.states.get(entity_id).state == STATE_OFF else: assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_device_is_active_coordinator(hass, zha_device_joined, zigpy_device): + """Test that the current coordinator is uniquely detected.""" + + current_coord_dev = zigpy_device(ieee="aa:bb:cc:dd:ee:ff:00:11", nwk=0x0000) + current_coord_dev.node_desc = current_coord_dev.node_desc.replace( + logical_type=zdo_t.LogicalType.Coordinator + ) + + old_coord_dev = zigpy_device(ieee="aa:bb:cc:dd:ee:ff:00:12", nwk=0x0000) + old_coord_dev.node_desc = old_coord_dev.node_desc.replace( + logical_type=zdo_t.LogicalType.Coordinator + ) + + # The two coordinators have different IEEE addresses + assert current_coord_dev.ieee != old_coord_dev.ieee + + current_coordinator = await zha_device_joined(current_coord_dev) + stale_coordinator = await zha_device_joined(old_coord_dev) + + # Ensure the current ApplicationController's IEEE matches our coordinator's + current_coordinator.gateway.application_controller.state.node_info.ieee = ( + current_coord_dev.ieee + ) + + assert current_coordinator.is_active_coordinator + assert not stale_coordinator.is_active_coordinator diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index fffb79fe0f2..f24ec054c0b 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -86,28 +86,28 @@ async def test_get_actions(hass, device_ias): "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_warningmode", + "entity_id": "select.fakemanufacturer_fakemodel_defaulttoneselect", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_sirenlevel", + "entity_id": "select.fakemanufacturer_fakemodel_defaultsirenlevelselect", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_strobelevel", + "entity_id": "select.fakemanufacturer_fakemodel_defaultstrobelevelselect", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_strobe", + "entity_id": "select.fakemanufacturer_fakemodel_defaultstrobeselect", "metadata": {"secondary": True}, }, ] diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 4de251fda8b..0f51141ec5d 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -439,8 +439,8 @@ def test_single_input_cluster_device_class_by_cluster_class(): @pytest.mark.parametrize( "override, entity_id", [ - (None, "light.manufacturer_model_77665544_level_light_color_on_off"), - ("switch", "switch.manufacturer_model_77665544_on_off"), + (None, "light.manufacturer_model_light"), + ("switch", "switch.manufacturer_model_switch"), ], ) async def test_device_override( diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index b19c98548ce..3c8c3e78c0e 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,7 +1,5 @@ """Test ZHA Gateway.""" import asyncio -import math -import time from unittest.mock import patch import pytest @@ -10,10 +8,9 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.core.store import TOMBSTONE_LIFETIME from homeassistant.const import Platform -from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway +from .common import async_find_group_entity_id, get_zha_gateway from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @@ -214,62 +211,3 @@ async def test_gateway_create_group_with_id(hass, device_light_1, coordinator): assert len(zha_group.members) == 1 assert zha_group.members[0].device is device_light_1 assert zha_group.group_id == 0x1234 - - -async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): - """Test saving data after a delay.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - await async_enable_traffic(hass, [zha_dev_basic]) - - assert zha_dev_basic.last_seen is not None - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - assert zha_dev_basic.last_seen is not None - last_seen = zha_dev_basic.last_seen - - # test that we can't set None as last seen any more - zha_dev_basic.async_update_last_seen(None) - assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - # test that we won't put None in storage - zigpy_dev_basic.last_seen = None - assert zha_dev_basic.last_seen is None - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) - - # test that we can still set a good last_seen - last_seen = time.time() - zha_dev_basic.async_update_last_seen(last_seen) - assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - # test that we still put good values in storage - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) - - -async def test_cleaning_up_storage(hass, zigpy_dev_basic, zha_dev_basic, hass_storage): - """Test cleaning up zha storage and remove stale devices.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - await async_enable_traffic(hass, [zha_dev_basic]) - - assert zha_dev_basic.last_seen is not None - await zha_gateway.zha_storage.async_save() - await hass.async_block_till_done() - - assert hass_storage["zha.storage"]["data"]["devices"] - device = hass_storage["zha.storage"]["data"]["devices"][0] - assert device["ieee"] == str(zha_dev_basic.ieee) - - zha_dev_basic.device.last_seen = time.time() - TOMBSTONE_LIFETIME - 1 - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - await zha_gateway.zha_storage.async_save() - await hass.async_block_till_done() - assert not hass_storage["zha.storage"]["data"]["devices"] diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 982ff622341..156f692aa14 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -15,17 +15,14 @@ from homeassistant.components.light import ( ColorMode, ) from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.light import ( - CAPABILITIES_COLOR_TEMP, - CAPABILITIES_COLOR_XY, - FLASH_EFFECTS, -) +from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform import homeassistant.util.dt as dt_util from .common import ( async_enable_traffic, async_find_group_entity_id, + async_shift_time, async_test_rejoin, find_entity_id, get_zha_gateway, @@ -148,7 +145,8 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -180,7 +178,8 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -239,7 +238,8 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -302,7 +302,7 @@ async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored ) @pytest.mark.parametrize( "device, reporting", - [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 3))], + [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 6))], ) async def test_light( hass, zigpy_device_mock, zha_device_joined_restored, device, reporting @@ -347,6 +347,7 @@ async def test_light( await async_test_level_on_off_from_hass( hass, cluster_on_off, cluster_level, entity_id ) + await async_shift_time(hass) # test getting a brightness change from the network await async_test_on_from_light(hass, cluster_on_off, entity_id) @@ -561,7 +562,7 @@ async def test_transitions( dev1_cluster_level.request.reset_mock() - # test non 0 length transition and color temp while turning light on (color_provided_while_off) + # test non 0 length transition and color temp while turning light on (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -597,7 +598,7 @@ async def test_transitions( 10, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, 235, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -646,7 +647,7 @@ async def test_transitions( dev1_cluster_color.request.reset_mock() dev1_cluster_level.request.reset_mock() - # test no transition provided and color temp while turning light on (color_provided_while_off) + # test no transition provided and color temp while turning light on (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -681,7 +682,7 @@ async def test_transitions( 10, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, 236, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -762,7 +763,7 @@ async def test_transitions( 10, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, 236, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -855,7 +856,7 @@ async def test_transitions( dev2_cluster_on_off.request.reset_mock() - # test non 0 length transition and color temp while turning light on and sengled (color_provided_while_off) + # test non 0 length transition and color temp while turning light on and sengled (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -891,7 +892,7 @@ async def test_transitions( 10, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, 235, # color temp mireds - 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -938,7 +939,7 @@ async def test_transitions( dev2_cluster_on_off.request.reset_mock() - # test non 0 length transition and color temp while turning group light on (color_provided_while_off) + # test non 0 length transition and color temp while turning group light on (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -961,13 +962,13 @@ async def test_transitions( assert group_level_channel.request.call_count == 1 assert group_level_channel.request.await_count == 1 - # groups are omitted from the 3 call dance for color_provided_while_off + # groups are omitted from the 3 call dance for new_color_provided_while_off assert group_color_channel.request.call_args == call( False, 10, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, 235, # color temp mireds - 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -1075,7 +1076,7 @@ async def test_transitions( dev2_cluster_level.request.reset_mock() - # test eWeLink color temp while turning light on from off (color_provided_while_off) + # test eWeLink color temp while turning light on from off (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -1185,12 +1186,14 @@ async def async_test_off_from_hass(hass, cluster, entity_id): async def async_test_level_on_off_from_hass( - hass, on_off_cluster, level_cluster, entity_id + hass, on_off_cluster, level_cluster, entity_id, expected_default_transition: int = 0 ): """Test on off functionality from hass.""" on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() + await async_shift_time(hass) + # turn on via UI await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True @@ -1211,6 +1214,8 @@ async def async_test_level_on_off_from_hass( on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() + await async_shift_time(hass) + await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -1260,7 +1265,7 @@ async def async_test_level_on_off_from_hass( 4, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 10, - 0, + expected_default_transition, expect_reply=True, manufacturer=None, tries=1, @@ -1400,7 +1405,7 @@ async def test_zha_group_light_entity( assert group_state.state == STATE_OFF assert group_state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, - ColorMode.HS, + ColorMode.XY, ] # Light which is off has no color mode assert "color_mode" not in group_state.attributes @@ -1408,19 +1413,29 @@ async def test_zha_group_light_entity( # test turning the lights on and off from the HA await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) + await async_shift_time(hass) + # test short flashing the lights from the HA await async_test_flash_from_hass( hass, group_cluster_identify, group_entity_id, FLASH_SHORT ) + await async_shift_time(hass) + # test turning the lights on and off from the light await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id) # test turning the lights on and off from the HA await async_test_level_on_off_from_hass( - hass, group_cluster_on_off, group_cluster_level, group_entity_id + hass, + group_cluster_on_off, + group_cluster_level, + group_entity_id, + expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition ) + await async_shift_time(hass) + # test getting a brightness change from the network await async_test_on_from_light(hass, dev1_cluster_on_off, group_entity_id) await async_test_dimmer_from_light( @@ -1431,15 +1446,17 @@ async def test_zha_group_light_entity( assert group_state.state == STATE_ON assert group_state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, - ColorMode.HS, + ColorMode.XY, ] - assert group_state.attributes["color_mode"] == ColorMode.HS + assert group_state.attributes["color_mode"] == ColorMode.XY # test long flashing the lights from the HA await async_test_flash_from_hass( hass, group_cluster_identify, group_entity_id, FLASH_LONG ) + await async_shift_time(hass) + assert len(zha_group.members) == 2 # test some of the group logic to make sure we key off states correctly await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index f6b606ccbbf..808649b7f55 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -133,7 +133,7 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi assert hass.states.get(entity_id).attributes.get("unit_of_measurement") == "%" assert ( hass.states.get(entity_id).attributes.get("friendly_name") - == "FakeManufacturer FakeModel e769900a analog_output PWM1" + == "FakeManufacturer FakeModel Number PWM1" ) # change value from device @@ -210,7 +210,7 @@ async def test_level_control_number( Platform.NUMBER, zha_device, hass, - qualifier=attr, + qualifier=attr.replace("_", ""), ) assert entity_id is not None diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index c883d648e8e..1c714def54b 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -113,12 +113,11 @@ async def test_select(hass, siren): entity_registry = er.async_get(hass) zha_device, cluster = siren assert cluster is not None - select_name = security.IasWd.Warning.WarningMode.__name__ entity_id = await find_entity_id( Platform.SELECT, zha_device, hass, - qualifier=select_name.lower(), + qualifier="tone", ) assert entity_id is not None @@ -163,7 +162,7 @@ async def test_select_restore_state( ): """Test zha select entity restore state.""" - entity_id = "select.fakemanufacturer_fakemodel_e769900a_ias_wd_warningmode" + entity_id = "select.fakemanufacturer_fakemodel_defaulttoneselect" core_rs(entity_id, state="Burglar") zigpy_device = zigpy_device_mock( @@ -180,12 +179,11 @@ async def test_select_restore_state( zha_device = await zha_device_restored(zigpy_device) cluster = zigpy_device.endpoints[1].ias_wd assert cluster is not None - select_name = security.IasWd.Warning.WarningMode.__name__ entity_id = await find_entity_id( Platform.SELECT, zha_device, hass, - qualifier=select_name.lower(), + qualifier="tone", ) assert entity_id is not None diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index c638bdd8c48..d2fc7c3ca73 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -48,7 +48,7 @@ from .common import ( ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_e769900a_{}" +ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_{}" @pytest.fixture(autouse=True) @@ -312,7 +312,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( smartenergy.Metering.cluster_id, - "smartenergy_metering_summation_delivered", + "smartenergy_summation", async_test_smart_energy_summation, 1, { @@ -360,7 +360,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( general.PowerConfiguration.cluster_id, - "power", + "battery", async_test_powerconfiguration, 2, { @@ -414,7 +414,7 @@ async def test_sensor( zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 cluster.PLUGGED_ATTR_READS = read_plug zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = ENTITY_ID_PREFIX.format(entity_suffix) + entity_id = ENTITY_ID_PREFIX.format(entity_suffix.replace("_", "")) await async_enable_traffic(hass, [zha_device], enabled=False) await hass.async_block_till_done() @@ -620,7 +620,7 @@ async def test_electrical_measurement_init( {"apparent_power", "rms_voltage", "rms_current"}, { "electrical_measurement", - "electrical_measurement_ac_frequency", + "electrical_measurement_frequency", "electrical_measurement_power_factor", }, { @@ -636,7 +636,7 @@ async def test_electrical_measurement_init( { "electrical_measurement_apparent_power", "electrical_measurement_rms_current", - "electrical_measurement_ac_frequency", + "electrical_measurement_frequency", "electrical_measurement_power_factor", }, ), @@ -648,7 +648,7 @@ async def test_electrical_measurement_init( "electrical_measurement", "electrical_measurement_apparent_power", "electrical_measurement_rms_current", - "electrical_measurement_ac_frequency", + "electrical_measurement_frequency", "electrical_measurement_power_factor", }, set(), @@ -659,7 +659,7 @@ async def test_electrical_measurement_init( "instantaneous_demand", }, { - "smartenergy_metering_summation_delivered", + "smartenergy_summation", }, { "smartenergy_metering", @@ -670,7 +670,7 @@ async def test_electrical_measurement_init( {"instantaneous_demand", "current_summ_delivered"}, {}, { - "smartenergy_metering_summation_delivered", + "smartenergy_summation", "smartenergy_metering", }, ), @@ -678,7 +678,7 @@ async def test_electrical_measurement_init( smartenergy.Metering.cluster_id, {}, { - "smartenergy_metering_summation_delivered", + "smartenergy_summation", "smartenergy_metering", }, {}, @@ -696,8 +696,10 @@ async def test_unsupported_attributes_sensor( ): """Test zha sensor platform.""" - entity_ids = {ENTITY_ID_PREFIX.format(e) for e in entity_ids} - missing_entity_ids = {ENTITY_ID_PREFIX.format(e) for e in missing_entity_ids} + entity_ids = {ENTITY_ID_PREFIX.format(e.replace("_", "")) for e in entity_ids} + missing_entity_ids = { + ENTITY_ID_PREFIX.format(e.replace("_", "")) for e in missing_entity_ids + } zigpy_device = zigpy_device_mock( { @@ -718,7 +720,7 @@ async def test_unsupported_attributes_sensor( await async_enable_traffic(hass, [zha_device], enabled=False) await hass.async_block_till_done() - present_entity_ids = set(await find_entity_ids(Platform.SENSOR, zha_device, hass)) + present_entity_ids = set(find_entity_ids(Platform.SENSOR, zha_device, hass)) assert present_entity_ids == entity_ids assert missing_entity_ids not in present_entity_ids @@ -811,7 +813,7 @@ async def test_se_summation_uom( ): """Test zha smart energy summation.""" - entity_id = ENTITY_ID_PREFIX.format("smartenergy_metering_summation_delivered") + entity_id = ENTITY_ID_PREFIX.format("smartenergysummation") zigpy_device = zigpy_device_mock( { 1: { @@ -865,7 +867,7 @@ async def test_elec_measurement_sensor_type( ): """Test zha electrical measurement sensor type.""" - entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + entity_id = ENTITY_ID_PREFIX.format("electricalmeasurement") zigpy_dev = elec_measurement_zigpy_dev zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[ "measurement_type" @@ -914,7 +916,7 @@ async def test_elec_measurement_skip_unsupported_attribute( ): """Test zha electrical measurement skipping update of unsupported attributes.""" - entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + entity_id = ENTITY_ID_PREFIX.format("electricalmeasurement") zha_dev = elec_measurement_zha_dev cluster = zha_dev.device.endpoints[1].electrical_measurement diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 9a32a1670b6..4cea8eb8f66 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -38,25 +38,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008"], DEV_SIG_ENTITIES: [ - "button.adurolight_adurolight_ncc_77665544_identify", - "sensor.adurolight_adurolight_ncc_77665544_basic_rssi", - "sensor.adurolight_adurolight_ncc_77665544_basic_lqi", + "button.adurolight_adurolight_ncc_identifybutton", + "sensor.adurolight_adurolight_ncc_rssi", + "sensor.adurolight_adurolight_ncc_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_lqi", }, }, }, @@ -76,43 +76,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["5:0x0019"], DEV_SIG_ENTITIES: [ - "button.bosch_isw_zpr1_wp13_77665544_identify", - "sensor.bosch_isw_zpr1_wp13_77665544_power", - "sensor.bosch_isw_zpr1_wp13_77665544_temperature", - "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", - "sensor.bosch_isw_zpr1_wp13_77665544_basic_rssi", - "sensor.bosch_isw_zpr1_wp13_77665544_basic_lqi", + "button.bosch_isw_zpr1_wp13_identifybutton", + "sensor.bosch_isw_zpr1_wp13_battery", + "sensor.bosch_isw_zpr1_wp13_temperature", + "binary_sensor.bosch_isw_zpr1_wp13_iaszone", + "sensor.bosch_isw_zpr1_wp13_rssi", + "sensor.bosch_isw_zpr1_wp13_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-5-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.bosch_isw_zpr1_wp13_iaszone", }, ("button", "00:11:22:33:44:55:66:77-5-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-5-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_battery", }, ("sensor", "00:11:22:33:44:55:66:77-5-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-5-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-5-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_lqi", }, }, }, @@ -132,31 +132,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3130_77665544_identify", - "sensor.centralite_3130_77665544_power", - "sensor.centralite_3130_77665544_basic_rssi", - "sensor.centralite_3130_77665544_basic_lqi", + "button.centralite_3130_identifybutton", + "sensor.centralite_3130_battery", + "sensor.centralite_3130_rssi", + "sensor.centralite_3130_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3130_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3130_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_lqi", }, }, }, @@ -176,79 +176,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3210_l_77665544_identify", - "sensor.centralite_3210_l_77665544_electrical_measurement", - "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", - "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", - "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", - "sensor.centralite_3210_l_77665544_electrical_measurement_ac_frequency", - "sensor.centralite_3210_l_77665544_electrical_measurement_power_factor", - "sensor.centralite_3210_l_77665544_smartenergy_metering", - "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", - "switch.centralite_3210_l_77665544_on_off", - "sensor.centralite_3210_l_77665544_basic_rssi", - "sensor.centralite_3210_l_77665544_basic_lqi", + "button.centralite_3210_l_identifybutton", + "sensor.centralite_3210_l_electricalmeasurement", + "sensor.centralite_3210_l_electricalmeasurementapparentpower", + "sensor.centralite_3210_l_electricalmeasurementrmscurrent", + "sensor.centralite_3210_l_electricalmeasurementrmsvoltage", + "sensor.centralite_3210_l_electricalmeasurementfrequency", + "sensor.centralite_3210_l_electricalmeasurementpowerfactor", + "sensor.centralite_3210_l_smartenergymetering", + "sensor.centralite_3210_l_smartenergysummation", + "switch.centralite_3210_l_switch", + "sensor.centralite_3210_l_rssi", + "sensor.centralite_3210_l_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.centralite_3210_l_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.centralite_3210_l_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_lqi", }, }, }, @@ -268,43 +268,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3310_s_77665544_identify", - "sensor.centralite_3310_s_77665544_power", - "sensor.centralite_3310_s_77665544_temperature", - "sensor.centralite_3310_s_77665544_manufacturer_specific", - "sensor.centralite_3310_s_77665544_basic_rssi", - "sensor.centralite_3310_s_77665544_basic_lqi", + "button.centralite_3310_s_identifybutton", + "sensor.centralite_3310_s_battery", + "sensor.centralite_3310_s_temperature", + "sensor.centralite_3310_s_humidity", + "sensor.centralite_3310_s_rssi", + "sensor.centralite_3310_s_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-64581"): { DEV_SIG_CHANNELS: ["manufacturer_specific"], DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_manufacturer_specific", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_humidity", }, }, }, @@ -331,43 +331,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3315_s_77665544_identify", - "sensor.centralite_3315_s_77665544_power", - "sensor.centralite_3315_s_77665544_temperature", - "binary_sensor.centralite_3315_s_77665544_ias_zone", - "sensor.centralite_3315_s_77665544_basic_rssi", - "sensor.centralite_3315_s_77665544_basic_lqi", + "button.centralite_3315_s_identifybutton", + "sensor.centralite_3315_s_battery", + "sensor.centralite_3315_s_temperature", + "binary_sensor.centralite_3315_s_iaszone", + "sensor.centralite_3315_s_rssi", + "sensor.centralite_3315_s_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3315_s_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3315_s_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_lqi", }, }, }, @@ -394,43 +394,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3320_l_77665544_identify", - "sensor.centralite_3320_l_77665544_power", - "sensor.centralite_3320_l_77665544_temperature", - "binary_sensor.centralite_3320_l_77665544_ias_zone", - "sensor.centralite_3320_l_77665544_basic_rssi", - "sensor.centralite_3320_l_77665544_basic_lqi", + "button.centralite_3320_l_identifybutton", + "sensor.centralite_3320_l_battery", + "sensor.centralite_3320_l_temperature", + "binary_sensor.centralite_3320_l_iaszone", + "sensor.centralite_3320_l_rssi", + "sensor.centralite_3320_l_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3320_l_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3320_l_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_lqi", }, }, }, @@ -457,43 +457,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3326_l_77665544_identify", - "sensor.centralite_3326_l_77665544_power", - "sensor.centralite_3326_l_77665544_temperature", - "binary_sensor.centralite_3326_l_77665544_ias_zone", - "sensor.centralite_3326_l_77665544_basic_rssi", - "sensor.centralite_3326_l_77665544_basic_lqi", + "button.centralite_3326_l_identifybutton", + "sensor.centralite_3326_l_battery", + "sensor.centralite_3326_l_temperature", + "binary_sensor.centralite_3326_l_iaszone", + "sensor.centralite_3326_l_rssi", + "sensor.centralite_3326_l_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3326_l_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3326_l_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_lqi", }, }, }, @@ -520,49 +520,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_motion_sensor_a_77665544_identify", - "sensor.centralite_motion_sensor_a_77665544_power", - "sensor.centralite_motion_sensor_a_77665544_temperature", - "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", - "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", - "sensor.centralite_motion_sensor_a_77665544_basic_rssi", - "sensor.centralite_motion_sensor_a_77665544_basic_lqi", + "button.centralite_motion_sensor_a_identifybutton", + "sensor.centralite_motion_sensor_a_battery", + "sensor.centralite_motion_sensor_a_temperature", + "binary_sensor.centralite_motion_sensor_a_iaszone", + "binary_sensor.centralite_motion_sensor_a_occupancy", + "sensor.centralite_motion_sensor_a_rssi", + "sensor.centralite_motion_sensor_a_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-1030"): { DEV_SIG_CHANNELS: ["occupancy"], DEV_SIG_ENT_MAP_CLASS: "Occupancy", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_occupancy", }, }, }, @@ -589,43 +589,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["4:0x0019"], DEV_SIG_ENTITIES: [ - "button.climaxtechnology_psmp5_00_00_02_02tc_77665544_identify", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", - "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_rssi", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_lqi", + "button.climaxtechnology_psmp5_00_00_02_02tc_identifybutton", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergymetering", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergysummation", + "switch.climaxtechnology_psmp5_00_00_02_02tc_switch", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_rssi", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.climaxtechnology_psmp5_00_00_02_02tc_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_psmp5_00_00_02_02tc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_psmp5_00_00_02_02tc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_lqi", }, }, }, @@ -645,61 +645,61 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", - "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", - "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_rssi", - "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_warningmode", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_sirenlevel", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobelevel", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobe", - "siren.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd", + "button.climaxtechnology_sd8sc_00_00_03_12tc_identifybutton", + "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_iaszone", + "sensor.climaxtechnology_sd8sc_00_00_03_12tc_rssi", + "sensor.climaxtechnology_sd8sc_00_00_03_12tc_lqi", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaulttoneselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultsirenlevelselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobelevelselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobeselect", + "siren.climaxtechnology_sd8sc_00_00_03_12tc_siren", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_lqi", }, ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_warningmode", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaulttoneselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_sirenlevel", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultsirenlevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobelevel", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobelevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobe", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobeselect", }, ("siren", "00:11:22:33:44:55:66:77-1-1282"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd", + DEV_SIG_ENT_MAP_ID: "siren.climaxtechnology_sd8sc_00_00_03_12tc_siren", }, }, }, @@ -719,31 +719,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", - "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", - "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_rssi", - "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_lqi", + "button.climaxtechnology_ws15_00_00_03_03tc_identifybutton", + "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_iaszone", + "sensor.climaxtechnology_ws15_00_00_03_03tc_rssi", + "sensor.climaxtechnology_ws15_00_00_03_03tc_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_lqi", }, }, }, @@ -770,31 +770,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", - "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", - "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_rssi", - "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_lqi", + "button.feibit_inc_co_fb56_zcw08ku1_1_identifybutton", + "light.feibit_inc_co_fb56_zcw08ku1_1_light", + "sensor.feibit_inc_co_fb56_zcw08ku1_1_rssi", + "sensor.feibit_inc_co_fb56_zcw08ku1_1_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-11"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.feibit_inc_co_fb56_zcw08ku1_1_light", }, ("button", "00:11:22:33:44:55:66:77-11-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-11-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-11-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_lqi", }, }, }, @@ -814,67 +814,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.heiman_smokesensor_em_77665544_identify", - "sensor.heiman_smokesensor_em_77665544_power", - "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", - "sensor.heiman_smokesensor_em_77665544_basic_rssi", - "sensor.heiman_smokesensor_em_77665544_basic_lqi", - "select.heiman_smokesensor_em_77665544_ias_wd_warningmode", - "select.heiman_smokesensor_em_77665544_ias_wd_sirenlevel", - "select.heiman_smokesensor_em_77665544_ias_wd_strobelevel", - "select.heiman_smokesensor_em_77665544_ias_wd_strobe", - "siren.heiman_smokesensor_em_77665544_ias_wd", + "button.heiman_smokesensor_em_identifybutton", + "sensor.heiman_smokesensor_em_battery", + "binary_sensor.heiman_smokesensor_em_iaszone", + "sensor.heiman_smokesensor_em_rssi", + "sensor.heiman_smokesensor_em_lqi", + "select.heiman_smokesensor_em_defaulttoneselect", + "select.heiman_smokesensor_em_defaultsirenlevelselect", + "select.heiman_smokesensor_em_defaultstrobelevelselect", + "select.heiman_smokesensor_em_defaultstrobeselect", + "siren.heiman_smokesensor_em_siren", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_smokesensor_em_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_lqi", }, ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_warningmode", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaulttoneselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_sirenlevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultsirenlevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_strobelevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultstrobelevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_strobe", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultstrobeselect", }, ("siren", "00:11:22:33:44:55:66:77-1-1282"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.heiman_smokesensor_em_77665544_ias_wd", + DEV_SIG_ENT_MAP_ID: "siren.heiman_smokesensor_em_siren", }, }, }, @@ -894,31 +894,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.heiman_co_v16_77665544_identify", - "binary_sensor.heiman_co_v16_77665544_ias_zone", - "sensor.heiman_co_v16_77665544_basic_rssi", - "sensor.heiman_co_v16_77665544_basic_lqi", + "button.heiman_co_v16_identifybutton", + "binary_sensor.heiman_co_v16_iaszone", + "sensor.heiman_co_v16_rssi", + "sensor.heiman_co_v16_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_co_v16_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_co_v16_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_lqi", }, }, }, @@ -938,61 +938,61 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.heiman_warningdevice_77665544_identify", - "binary_sensor.heiman_warningdevice_77665544_ias_zone", - "sensor.heiman_warningdevice_77665544_basic_rssi", - "sensor.heiman_warningdevice_77665544_basic_lqi", - "select.heiman_warningdevice_77665544_ias_wd_warningmode", - "select.heiman_warningdevice_77665544_ias_wd_sirenlevel", - "select.heiman_warningdevice_77665544_ias_wd_strobelevel", - "select.heiman_warningdevice_77665544_ias_wd_strobe", - "siren.heiman_warningdevice_77665544_ias_wd", + "button.heiman_warningdevice_identifybutton", + "binary_sensor.heiman_warningdevice_iaszone", + "sensor.heiman_warningdevice_rssi", + "sensor.heiman_warningdevice_lqi", + "select.heiman_warningdevice_defaulttoneselect", + "select.heiman_warningdevice_defaultsirenlevelselect", + "select.heiman_warningdevice_defaultstrobelevelselect", + "select.heiman_warningdevice_defaultstrobeselect", + "siren.heiman_warningdevice_siren", ], DEV_SIG_ENT_MAP: { ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_warningmode", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaulttoneselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_sirenlevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultsirenlevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_strobelevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultstrobelevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_strobe", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultstrobeselect", }, ("siren", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.heiman_warningdevice_77665544_ias_wd", + DEV_SIG_ENT_MAP_ID: "siren.heiman_warningdevice_siren", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_lqi", }, }, }, @@ -1012,49 +1012,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["6:0x0019"], DEV_SIG_ENTITIES: [ - "button.hivehome_com_mot003_77665544_identify", - "sensor.hivehome_com_mot003_77665544_power", - "sensor.hivehome_com_mot003_77665544_illuminance", - "sensor.hivehome_com_mot003_77665544_temperature", - "binary_sensor.hivehome_com_mot003_77665544_ias_zone", - "sensor.hivehome_com_mot003_77665544_basic_rssi", - "sensor.hivehome_com_mot003_77665544_basic_lqi", + "button.hivehome_com_mot003_identifybutton", + "sensor.hivehome_com_mot003_battery", + "sensor.hivehome_com_mot003_illuminance", + "sensor.hivehome_com_mot003_temperature", + "binary_sensor.hivehome_com_mot003_iaszone", + "sensor.hivehome_com_mot003_rssi", + "sensor.hivehome_com_mot003_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-6-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.hivehome_com_mot003_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.hivehome_com_mot003_iaszone", }, ("button", "00:11:22:33:44:55:66:77-6-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-6-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_battery", }, ("sensor", "00:11:22:33:44:55:66:77-6-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-6-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-6-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-6-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_lqi", }, }, }, @@ -1081,31 +1081,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_lqi", }, }, }, @@ -1125,31 +1125,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_lqi", }, }, }, @@ -1169,31 +1169,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_lqi", }, }, }, @@ -1213,31 +1213,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_lqi", }, }, }, @@ -1257,31 +1257,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_lqi", }, }, }, @@ -1301,31 +1301,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", - "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", - "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_control_outlet_identifybutton", + "switch.ikea_of_sweden_tradfri_control_outlet_switch", + "sensor.ikea_of_sweden_tradfri_control_outlet_rssi", + "sensor.ikea_of_sweden_tradfri_control_outlet_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.ikea_of_sweden_tradfri_control_outlet_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_lqi", }, }, }, @@ -1345,37 +1345,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_motion_sensor_77665544_identify", - "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", - "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", - "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_motion_sensor_identifybutton", + "sensor.ikea_of_sweden_tradfri_motion_sensor_battery", + "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_motion", + "sensor.ikea_of_sweden_tradfri_motion_sensor_rssi", + "sensor.ikea_of_sweden_tradfri_motion_sensor_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_motion_sensor_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_motion_sensor_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Motion", - DEV_SIG_ENT_MAP_ID: "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_motion", }, }, }, @@ -1395,31 +1395,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0102"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_on_off_switch_77665544_identify", - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_on_off_switch_identifybutton", + "sensor.ikea_of_sweden_tradfri_on_off_switch_battery", + "sensor.ikea_of_sweden_tradfri_on_off_switch_rssi", + "sensor.ikea_of_sweden_tradfri_on_off_switch_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_on_off_switch_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_on_off_switch_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_lqi", }, }, }, @@ -1439,31 +1439,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_remote_control_77665544_identify", - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_remote_control_identifybutton", + "sensor.ikea_of_sweden_tradfri_remote_control_battery", + "sensor.ikea_of_sweden_tradfri_remote_control_rssi", + "sensor.ikea_of_sweden_tradfri_remote_control_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_remote_control_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_remote_control_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_lqi", }, }, }, @@ -1490,25 +1490,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", - "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_signal_repeater_identifybutton", + "sensor.ikea_of_sweden_tradfri_signal_repeater_rssi", + "sensor.ikea_of_sweden_tradfri_signal_repeater_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_lqi", }, }, }, @@ -1528,31 +1528,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_wireless_dimmer_77665544_identify", - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_wireless_dimmer_identifybutton", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_battery", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_rssi", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_wireless_dimmer_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_wireless_dimmer_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_lqi", }, }, }, @@ -1579,43 +1579,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ - "button.jasco_products_45852_77665544_identify", - "sensor.jasco_products_45852_77665544_smartenergy_metering", - "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", - "light.jasco_products_45852_77665544_level_on_off", - "sensor.jasco_products_45852_77665544_basic_rssi", - "sensor.jasco_products_45852_77665544_basic_lqi", + "button.jasco_products_45852_identifybutton", + "sensor.jasco_products_45852_smartenergymetering", + "sensor.jasco_products_45852_smartenergysummation", + "light.jasco_products_45852_light", + "sensor.jasco_products_45852_rssi", + "sensor.jasco_products_45852_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45852_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.jasco_products_45852_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_lqi", }, }, }, @@ -1642,43 +1642,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.jasco_products_45856_77665544_identify", - "light.jasco_products_45856_77665544_on_off", - "sensor.jasco_products_45856_77665544_smartenergy_metering", - "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", - "sensor.jasco_products_45856_77665544_basic_rssi", - "sensor.jasco_products_45856_77665544_basic_lqi", + "button.jasco_products_45856_identifybutton", + "light.jasco_products_45856_light", + "sensor.jasco_products_45856_smartenergymetering", + "sensor.jasco_products_45856_smartenergysummation", + "sensor.jasco_products_45856_rssi", + "sensor.jasco_products_45856_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45856_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.jasco_products_45856_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_lqi", }, }, }, @@ -1705,43 +1705,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ - "button.jasco_products_45857_77665544_identify", - "light.jasco_products_45857_77665544_level_on_off", - "sensor.jasco_products_45857_77665544_smartenergy_metering", - "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", - "sensor.jasco_products_45857_77665544_basic_rssi", - "sensor.jasco_products_45857_77665544_basic_lqi", + "button.jasco_products_45857_identifybutton", + "light.jasco_products_45857_light", + "sensor.jasco_products_45857_smartenergymetering", + "sensor.jasco_products_45857_smartenergysummation", + "sensor.jasco_products_45857_rssi", + "sensor.jasco_products_45857_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45857_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.jasco_products_45857_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_lqi", }, }, }, @@ -1761,49 +1761,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.keen_home_inc_sv02_610_mp_1_3_77665544_identify", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", - "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_rssi", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_lqi", + "button.keen_home_inc_sv02_610_mp_1_3_identifybutton", + "sensor.keen_home_inc_sv02_610_mp_1_3_battery", + "sensor.keen_home_inc_sv02_610_mp_1_3_pressure", + "sensor.keen_home_inc_sv02_610_mp_1_3_temperature", + "cover.keen_home_inc_sv02_610_mp_1_3_keenvent", + "sensor.keen_home_inc_sv02_610_mp_1_3_rssi", + "sensor.keen_home_inc_sv02_610_mp_1_3_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_610_mp_1_3_keenvent", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_lqi", }, }, }, @@ -1823,49 +1823,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.keen_home_inc_sv02_612_mp_1_2_77665544_identify", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", - "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_rssi", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_lqi", + "button.keen_home_inc_sv02_612_mp_1_2_identifybutton", + "sensor.keen_home_inc_sv02_612_mp_1_2_battery", + "sensor.keen_home_inc_sv02_612_mp_1_2_pressure", + "sensor.keen_home_inc_sv02_612_mp_1_2_temperature", + "cover.keen_home_inc_sv02_612_mp_1_2_keenvent", + "sensor.keen_home_inc_sv02_612_mp_1_2_rssi", + "sensor.keen_home_inc_sv02_612_mp_1_2_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_2_keenvent", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_lqi", }, }, }, @@ -1885,49 +1885,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.keen_home_inc_sv02_612_mp_1_3_77665544_identify", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", - "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_rssi", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_lqi", + "button.keen_home_inc_sv02_612_mp_1_3_identifybutton", + "sensor.keen_home_inc_sv02_612_mp_1_3_battery", + "sensor.keen_home_inc_sv02_612_mp_1_3_pressure", + "sensor.keen_home_inc_sv02_612_mp_1_3_temperature", + "cover.keen_home_inc_sv02_612_mp_1_3_keenvent", + "sensor.keen_home_inc_sv02_612_mp_1_3_rssi", + "sensor.keen_home_inc_sv02_612_mp_1_3_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_3_keenvent", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_lqi", }, }, }, @@ -1947,37 +1947,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", - "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", - "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", - "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_rssi", - "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_lqi", + "button.king_of_fans_inc_hbuniversalcfremote_identifybutton", + "light.king_of_fans_inc_hbuniversalcfremote_light", + "fan.king_of_fans_inc_hbuniversalcfremote_fan", + "sensor.king_of_fans_inc_hbuniversalcfremote_rssi", + "sensor.king_of_fans_inc_hbuniversalcfremote_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.king_of_fans_inc_hbuniversalcfremote_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_lqi", }, ("fan", "00:11:22:33:44:55:66:77-1-514"): { DEV_SIG_CHANNELS: ["fan"], DEV_SIG_ENT_MAP_CLASS: "ZhaFan", - DEV_SIG_ENT_MAP_ID: "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", + DEV_SIG_ENT_MAP_ID: "fan.king_of_fans_inc_hbuniversalcfremote_fan", }, }, }, @@ -1997,31 +1997,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0300"], DEV_SIG_ENTITIES: [ - "button.lds_zbt_cctswitch_d0001_77665544_identify", - "sensor.lds_zbt_cctswitch_d0001_77665544_power", - "sensor.lds_zbt_cctswitch_d0001_77665544_basic_rssi", - "sensor.lds_zbt_cctswitch_d0001_77665544_basic_lqi", + "button.lds_zbt_cctswitch_d0001_identifybutton", + "sensor.lds_zbt_cctswitch_d0001_battery", + "sensor.lds_zbt_cctswitch_d0001_rssi", + "sensor.lds_zbt_cctswitch_d0001_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_lqi", }, }, }, @@ -2041,31 +2041,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_a19_rgbw_77665544_identify", - "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", - "sensor.ledvance_a19_rgbw_77665544_basic_rssi", - "sensor.ledvance_a19_rgbw_77665544_basic_lqi", + "button.ledvance_a19_rgbw_identifybutton", + "light.ledvance_a19_rgbw_light", + "sensor.ledvance_a19_rgbw_rssi", + "sensor.ledvance_a19_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_lqi", }, }, }, @@ -2085,31 +2085,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_flex_rgbw_77665544_identify", - "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", - "sensor.ledvance_flex_rgbw_77665544_basic_rssi", - "sensor.ledvance_flex_rgbw_77665544_basic_lqi", + "button.ledvance_flex_rgbw_identifybutton", + "light.ledvance_flex_rgbw_light", + "sensor.ledvance_flex_rgbw_rssi", + "sensor.ledvance_flex_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_lqi", }, }, }, @@ -2129,31 +2129,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_plug_77665544_identify", - "switch.ledvance_plug_77665544_on_off", - "sensor.ledvance_plug_77665544_basic_rssi", - "sensor.ledvance_plug_77665544_basic_lqi", + "button.ledvance_plug_identifybutton", + "switch.ledvance_plug_switch", + "sensor.ledvance_plug_rssi", + "sensor.ledvance_plug_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.ledvance_plug_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.ledvance_plug_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_lqi", }, }, }, @@ -2173,31 +2173,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_rt_rgbw_77665544_identify", - "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", - "sensor.ledvance_rt_rgbw_77665544_basic_rssi", - "sensor.ledvance_rt_rgbw_77665544_basic_lqi", + "button.ledvance_rt_rgbw_identifybutton", + "light.ledvance_rt_rgbw_light", + "sensor.ledvance_rt_rgbw_rssi", + "sensor.ledvance_rt_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_lqi", }, }, }, @@ -2238,91 +2238,91 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_plug_maus01_77665544_identify", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_apparent_power", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_ac_frequency", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_power_factor", - "sensor.lumi_lumi_plug_maus01_77665544_analog_input", - "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", - "binary_sensor.lumi_lumi_plug_maus01_77665544_binary_input", - "switch.lumi_lumi_plug_maus01_77665544_on_off", - "sensor.lumi_lumi_plug_maus01_77665544_basic_rssi", - "sensor.lumi_lumi_plug_maus01_77665544_basic_lqi", - "sensor.lumi_lumi_plug_maus01_77665544_device_temperature", + "button.lumi_lumi_plug_maus01_identifybutton", + "sensor.lumi_lumi_plug_maus01_electricalmeasurement", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementapparentpower", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmscurrent", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmsvoltage", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementfrequency", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementpowerfactor", + "sensor.lumi_lumi_plug_maus01_analoginput", + "sensor.lumi_lumi_plug_maus01_analoginput_2", + "binary_sensor.lumi_lumi_plug_maus01_binaryinput", + "switch.lumi_lumi_plug_maus01_switch", + "sensor.lumi_lumi_plug_maus01_rssi", + "sensor.lumi_lumi_plug_maus01_lqi", + "sensor.lumi_lumi_plug_maus01_devicetemperature", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.lumi_lumi_plug_maus01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.lumi_lumi_plug_maus01_switch", }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_device_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_devicetemperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-2-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_analoginput", }, ("sensor", "00:11:22:33:44:55:66:77-3-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_analoginput_2", }, ("binary_sensor", "00:11:22:33:44:55:66:77-100-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_plug_maus01_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_plug_maus01_binaryinput", }, }, }, @@ -2349,79 +2349,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_relay_c2acn01_77665544_identify", - "light.lumi_lumi_relay_c2acn01_77665544_on_off", - "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_apparent_power", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_ac_frequency", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_power_factor", - "sensor.lumi_lumi_relay_c2acn01_77665544_basic_rssi", - "sensor.lumi_lumi_relay_c2acn01_77665544_basic_lqi", - "sensor.lumi_lumi_relay_c2acn01_77665544_device_temperature", + "button.lumi_lumi_relay_c2acn01_identifybutton", + "light.lumi_lumi_relay_c2acn01_light", + "light.lumi_lumi_relay_c2acn01_light_2", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurement", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementapparentpower", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmscurrent", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmsvoltage", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementfrequency", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementpowerfactor", + "sensor.lumi_lumi_relay_c2acn01_rssi", + "sensor.lumi_lumi_relay_c2acn01_lqi", + "sensor.lumi_lumi_relay_c2acn01_devicetemperature", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_light", }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_device_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_devicetemperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_lqi", }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_light_2", }, }, }, @@ -2455,31 +2455,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b186acn01_77665544_identify", - "sensor.lumi_lumi_remote_b186acn01_77665544_power", - "sensor.lumi_lumi_remote_b186acn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b186acn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b186acn01_identifybutton", + "sensor.lumi_lumi_remote_b186acn01_battery", + "sensor.lumi_lumi_remote_b186acn01_rssi", + "sensor.lumi_lumi_remote_b186acn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_lqi", }, }, }, @@ -2513,31 +2513,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b286acn01_77665544_identify", - "sensor.lumi_lumi_remote_b286acn01_77665544_power", - "sensor.lumi_lumi_remote_b286acn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b286acn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b286acn01_identifybutton", + "sensor.lumi_lumi_remote_b286acn01_battery", + "sensor.lumi_lumi_remote_b286acn01_rssi", + "sensor.lumi_lumi_remote_b286acn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_lqi", }, }, }, @@ -2592,25 +2592,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b286opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b286opcn01_identifybutton", + "sensor.lumi_lumi_remote_b286opcn01_rssi", + "sensor.lumi_lumi_remote_b286opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_lqi", }, }, }, @@ -2665,25 +2665,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b486opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b486opcn01_identifybutton", + "sensor.lumi_lumi_remote_b486opcn01_rssi", + "sensor.lumi_lumi_remote_b486opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_lqi", }, }, }, @@ -2703,25 +2703,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b686opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b686opcn01_identifybutton", + "sensor.lumi_lumi_remote_b686opcn01_rssi", + "sensor.lumi_lumi_remote_b686opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_lqi", }, }, }, @@ -2776,25 +2776,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b686opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b686opcn01_identifybutton", + "sensor.lumi_lumi_remote_b686opcn01_rssi", + "sensor.lumi_lumi_remote_b686opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_lqi", }, }, }, @@ -2814,31 +2814,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], DEV_SIG_ENTITIES: [ - "light.lumi_lumi_router_77665544_on_off", - "binary_sensor.lumi_lumi_router_77665544_on_off", - "sensor.lumi_lumi_router_77665544_basic_rssi", - "sensor.lumi_lumi_router_77665544_basic_lqi", + "light.lumi_lumi_router_light", + "binary_sensor.lumi_lumi_router_opening", + "sensor.lumi_lumi_router_rssi", + "sensor.lumi_lumi_router_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", }, }, }, @@ -2858,31 +2858,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], DEV_SIG_ENTITIES: [ - "light.lumi_lumi_router_77665544_on_off", - "binary_sensor.lumi_lumi_router_77665544_on_off", - "sensor.lumi_lumi_router_77665544_basic_rssi", - "sensor.lumi_lumi_router_77665544_basic_lqi", + "light.lumi_lumi_router_light", + "binary_sensor.lumi_lumi_router_opening", + "sensor.lumi_lumi_router_rssi", + "sensor.lumi_lumi_router_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", }, }, }, @@ -2902,31 +2902,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], DEV_SIG_ENTITIES: [ - "light.lumi_lumi_router_77665544_on_off", - "binary_sensor.lumi_lumi_router_77665544_on_off", - "sensor.lumi_lumi_router_77665544_basic_rssi", - "sensor.lumi_lumi_router_77665544_basic_lqi", + "light.lumi_lumi_router_light", + "binary_sensor.lumi_lumi_router_opening", + "sensor.lumi_lumi_router_rssi", + "sensor.lumi_lumi_router_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", }, }, }, @@ -2946,31 +2946,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sen_ill_mgl01_77665544_identify", - "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", - "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_rssi", - "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_lqi", + "button.lumi_lumi_sen_ill_mgl01_identifybutton", + "sensor.lumi_lumi_sen_ill_mgl01_illuminance", + "sensor.lumi_lumi_sen_ill_mgl01_rssi", + "sensor.lumi_lumi_sen_ill_mgl01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_lqi", }, }, }, @@ -3004,31 +3004,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_86sw1_77665544_identify", - "sensor.lumi_lumi_sensor_86sw1_77665544_power", - "sensor.lumi_lumi_sensor_86sw1_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_86sw1_77665544_basic_lqi", + "button.lumi_lumi_sensor_86sw1_identifybutton", + "sensor.lumi_lumi_sensor_86sw1_battery", + "sensor.lumi_lumi_sensor_86sw1_rssi", + "sensor.lumi_lumi_sensor_86sw1_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_lqi", }, }, }, @@ -3062,31 +3062,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_cube_aqgl01_77665544_identify", - "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", - "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_lqi", + "button.lumi_lumi_sensor_cube_aqgl01_identifybutton", + "sensor.lumi_lumi_sensor_cube_aqgl01_battery", + "sensor.lumi_lumi_sensor_cube_aqgl01_rssi", + "sensor.lumi_lumi_sensor_cube_aqgl01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_lqi", }, }, }, @@ -3120,43 +3120,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_ht_77665544_identify", - "sensor.lumi_lumi_sensor_ht_77665544_power", - "sensor.lumi_lumi_sensor_ht_77665544_temperature", - "sensor.lumi_lumi_sensor_ht_77665544_humidity", - "sensor.lumi_lumi_sensor_ht_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_ht_77665544_basic_lqi", + "button.lumi_lumi_sensor_ht_identifybutton", + "sensor.lumi_lumi_sensor_ht_battery", + "sensor.lumi_lumi_sensor_ht_temperature", + "sensor.lumi_lumi_sensor_ht_humidity", + "sensor.lumi_lumi_sensor_ht_rssi", + "sensor.lumi_lumi_sensor_ht_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { DEV_SIG_CHANNELS: ["humidity"], DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_humidity", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_humidity", }, }, }, @@ -3176,37 +3176,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_magnet_77665544_identify", - "sensor.lumi_lumi_sensor_magnet_77665544_power", - "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", - "sensor.lumi_lumi_sensor_magnet_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_magnet_77665544_basic_lqi", + "button.lumi_lumi_sensor_magnet_identifybutton", + "sensor.lumi_lumi_sensor_magnet_battery", + "binary_sensor.lumi_lumi_sensor_magnet_opening", + "sensor.lumi_lumi_sensor_magnet_rssi", + "sensor.lumi_lumi_sensor_magnet_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_opening", }, }, }, @@ -3226,37 +3226,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_magnet_aq2_77665544_identify", - "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", - "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", - "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_lqi", + "button.lumi_lumi_sensor_magnet_aq2_identifybutton", + "sensor.lumi_lumi_sensor_magnet_aq2_battery", + "binary_sensor.lumi_lumi_sensor_magnet_aq2_opening", + "sensor.lumi_lumi_sensor_magnet_aq2_rssi", + "sensor.lumi_lumi_sensor_magnet_aq2_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_aq2_opening", }, }, }, @@ -3276,49 +3276,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_motion_aq2_77665544_identify", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", - "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", - "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_lqi", + "button.lumi_lumi_sensor_motion_aq2_identifybutton", + "sensor.lumi_lumi_sensor_motion_aq2_battery", + "sensor.lumi_lumi_sensor_motion_aq2_illuminance", + "binary_sensor.lumi_lumi_sensor_motion_aq2_occupancy", + "binary_sensor.lumi_lumi_sensor_motion_aq2_iaszone", + "sensor.lumi_lumi_sensor_motion_aq2_rssi", + "sensor.lumi_lumi_sensor_motion_aq2_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1030"): { DEV_SIG_CHANNELS: ["occupancy"], DEV_SIG_ENT_MAP_CLASS: "Occupancy", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_occupancy", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_lqi", }, }, }, @@ -3338,37 +3338,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_smoke_77665544_identify", - "sensor.lumi_lumi_sensor_smoke_77665544_power", - "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", - "sensor.lumi_lumi_sensor_smoke_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_smoke_77665544_basic_lqi", + "button.lumi_lumi_sensor_smoke_identifybutton", + "sensor.lumi_lumi_sensor_smoke_battery", + "binary_sensor.lumi_lumi_sensor_smoke_iaszone", + "sensor.lumi_lumi_sensor_smoke_rssi", + "sensor.lumi_lumi_sensor_smoke_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_smoke_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_lqi", }, }, }, @@ -3388,31 +3388,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_switch_77665544_identify", - "sensor.lumi_lumi_sensor_switch_77665544_power", - "sensor.lumi_lumi_sensor_switch_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_switch_77665544_basic_lqi", + "button.lumi_lumi_sensor_switch_identifybutton", + "sensor.lumi_lumi_sensor_switch_battery", + "sensor.lumi_lumi_sensor_switch_rssi", + "sensor.lumi_lumi_sensor_switch_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_lqi", }, }, }, @@ -3432,25 +3432,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", - "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_lqi", + "sensor.lumi_lumi_sensor_switch_aq2_battery", + "sensor.lumi_lumi_sensor_switch_aq2_rssi", + "sensor.lumi_lumi_sensor_switch_aq2_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_lqi", }, }, }, @@ -3470,25 +3470,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", - "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_lqi", + "sensor.lumi_lumi_sensor_switch_aq3_battery", + "sensor.lumi_lumi_sensor_switch_aq3_rssi", + "sensor.lumi_lumi_sensor_switch_aq3_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_lqi", }, }, }, @@ -3508,43 +3508,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_wleak_aq1_77665544_identify", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", - "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_lqi", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_device_temperature", + "button.lumi_lumi_sensor_wleak_aq1_identifybutton", + "sensor.lumi_lumi_sensor_wleak_aq1_battery", + "binary_sensor.lumi_lumi_sensor_wleak_aq1_iaszone", + "sensor.lumi_lumi_sensor_wleak_aq1_rssi", + "sensor.lumi_lumi_sensor_wleak_aq1_lqi", + "sensor.lumi_lumi_sensor_wleak_aq1_devicetemperature", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_wleak_aq1_iaszone", }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_device_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_devicetemperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_lqi", }, }, }, @@ -3571,43 +3571,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_vibration_aq1_77665544_identify", - "sensor.lumi_lumi_vibration_aq1_77665544_power", - "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", - "lock.lumi_lumi_vibration_aq1_77665544_door_lock", - "sensor.lumi_lumi_vibration_aq1_77665544_basic_rssi", - "sensor.lumi_lumi_vibration_aq1_77665544_basic_lqi", + "button.lumi_lumi_vibration_aq1_identifybutton", + "sensor.lumi_lumi_vibration_aq1_battery", + "binary_sensor.lumi_lumi_vibration_aq1_iaszone", + "lock.lumi_lumi_vibration_aq1_doorlock", + "sensor.lumi_lumi_vibration_aq1_rssi", + "sensor.lumi_lumi_vibration_aq1_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_vibration_aq1_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_lqi", }, ("lock", "00:11:22:33:44:55:66:77-1-257"): { DEV_SIG_CHANNELS: ["door_lock"], DEV_SIG_ENT_MAP_CLASS: "ZhaDoorLock", - DEV_SIG_ENT_MAP_ID: "lock.lumi_lumi_vibration_aq1_77665544_door_lock", + DEV_SIG_ENT_MAP_ID: "lock.lumi_lumi_vibration_aq1_doorlock", }, }, }, @@ -3627,49 +3627,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_weather_77665544_identify", - "sensor.lumi_lumi_weather_77665544_power", - "sensor.lumi_lumi_weather_77665544_pressure", - "sensor.lumi_lumi_weather_77665544_temperature", - "sensor.lumi_lumi_weather_77665544_humidity", - "sensor.lumi_lumi_weather_77665544_basic_rssi", - "sensor.lumi_lumi_weather_77665544_basic_lqi", + "button.lumi_lumi_weather_identifybutton", + "sensor.lumi_lumi_weather_battery", + "sensor.lumi_lumi_weather_pressure", + "sensor.lumi_lumi_weather_temperature", + "sensor.lumi_lumi_weather_humidity", + "sensor.lumi_lumi_weather_rssi", + "sensor.lumi_lumi_weather_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { DEV_SIG_CHANNELS: ["humidity"], DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_humidity", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_humidity", }, }, }, @@ -3689,37 +3689,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.nyce_3010_77665544_identify", - "sensor.nyce_3010_77665544_power", - "binary_sensor.nyce_3010_77665544_ias_zone", - "sensor.nyce_3010_77665544_basic_rssi", - "sensor.nyce_3010_77665544_basic_lqi", + "button.nyce_3010_identifybutton", + "sensor.nyce_3010_battery", + "binary_sensor.nyce_3010_iaszone", + "sensor.nyce_3010_rssi", + "sensor.nyce_3010_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3010_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3010_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.nyce_3010_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.nyce_3010_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_lqi", }, }, }, @@ -3739,37 +3739,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.nyce_3014_77665544_identify", - "sensor.nyce_3014_77665544_power", - "binary_sensor.nyce_3014_77665544_ias_zone", - "sensor.nyce_3014_77665544_basic_rssi", - "sensor.nyce_3014_77665544_basic_lqi", + "button.nyce_3014_identifybutton", + "sensor.nyce_3014_battery", + "binary_sensor.nyce_3014_iaszone", + "sensor.nyce_3014_rssi", + "sensor.nyce_3014_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3014_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3014_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.nyce_3014_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.nyce_3014_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_lqi", }, }, }, @@ -3832,31 +3832,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_a19_rgbw_77665544_identify", - "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", - "sensor.osram_lightify_a19_rgbw_77665544_basic_rssi", - "sensor.osram_lightify_a19_rgbw_77665544_basic_lqi", + "button.osram_lightify_a19_rgbw_identifybutton", + "light.osram_lightify_a19_rgbw_light", + "sensor.osram_lightify_a19_rgbw_rssi", + "sensor.osram_lightify_a19_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.osram_lightify_a19_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_lqi", }, }, }, @@ -3876,31 +3876,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_dimming_switch_77665544_identify", - "sensor.osram_lightify_dimming_switch_77665544_power", - "sensor.osram_lightify_dimming_switch_77665544_basic_rssi", - "sensor.osram_lightify_dimming_switch_77665544_basic_lqi", + "button.osram_lightify_dimming_switch_identifybutton", + "sensor.osram_lightify_dimming_switch_battery", + "sensor.osram_lightify_dimming_switch_rssi", + "sensor.osram_lightify_dimming_switch_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_lqi", }, }, }, @@ -3920,31 +3920,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_flex_rgbw_77665544_identify", - "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", - "sensor.osram_lightify_flex_rgbw_77665544_basic_rssi", - "sensor.osram_lightify_flex_rgbw_77665544_basic_lqi", + "button.osram_lightify_flex_rgbw_identifybutton", + "light.osram_lightify_flex_rgbw_light", + "sensor.osram_lightify_flex_rgbw_rssi", + "sensor.osram_lightify_flex_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.osram_lightify_flex_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_lqi", }, }, }, @@ -3964,67 +3964,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_rt_tunable_white_77665544_identify", - "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_apparent_power", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_ac_frequency", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_power_factor", - "sensor.osram_lightify_rt_tunable_white_77665544_basic_rssi", - "sensor.osram_lightify_rt_tunable_white_77665544_basic_lqi", + "button.osram_lightify_rt_tunable_white_identifybutton", + "light.osram_lightify_rt_tunable_white_light", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurement", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementapparentpower", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmscurrent", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmsvoltage", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementfrequency", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementpowerfactor", + "sensor.osram_lightify_rt_tunable_white_rssi", + "sensor.osram_lightify_rt_tunable_white_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.osram_lightify_rt_tunable_white_light", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_lqi", }, }, }, @@ -4044,67 +4044,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_plug_01_77665544_identify", - "sensor.osram_plug_01_77665544_electrical_measurement", - "sensor.osram_plug_01_77665544_electrical_measurement_apparent_power", - "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", - "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", - "sensor.osram_plug_01_77665544_electrical_measurement_ac_frequency", - "sensor.osram_plug_01_77665544_electrical_measurement_power_factor", - "switch.osram_plug_01_77665544_on_off", - "sensor.osram_plug_01_77665544_basic_rssi", - "sensor.osram_plug_01_77665544_basic_lqi", + "button.osram_plug_01_identifybutton", + "sensor.osram_plug_01_electricalmeasurement", + "sensor.osram_plug_01_electricalmeasurementapparentpower", + "sensor.osram_plug_01_electricalmeasurementrmscurrent", + "sensor.osram_plug_01_electricalmeasurementrmsvoltage", + "sensor.osram_plug_01_electricalmeasurementfrequency", + "sensor.osram_plug_01_electricalmeasurementpowerfactor", + "switch.osram_plug_01_switch", + "sensor.osram_plug_01_rssi", + "sensor.osram_plug_01_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.osram_plug_01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.osram_plug_01_switch", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_lqi", }, }, }, @@ -4185,25 +4185,25 @@ DEVICES = [ "6:0x0300", ], DEV_SIG_ENTITIES: [ - "sensor.osram_switch_4x_lightify_77665544_power", - "sensor.osram_switch_4x_lightify_77665544_basic_rssi", - "sensor.osram_switch_4x_lightify_77665544_basic_lqi", + "sensor.osram_switch_4x_lightify_battery", + "sensor.osram_switch_4x_lightify_rssi", + "sensor.osram_switch_4x_lightify_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_lqi", }, }, }, @@ -4230,37 +4230,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "2:0x0019"], DEV_SIG_ENTITIES: [ - "button.philips_rwl020_77665544_identify", - "sensor.philips_rwl020_77665544_power", - "binary_sensor.philips_rwl020_77665544_binary_input", - "sensor.philips_rwl020_77665544_basic_rssi", - "sensor.philips_rwl020_77665544_basic_lqi", + "button.philips_rwl020_identifybutton", + "sensor.philips_rwl020_battery", + "binary_sensor.philips_rwl020_binaryinput", + "sensor.philips_rwl020_rssi", + "sensor.philips_rwl020_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_rwl020_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_rwl020_binaryinput", }, ("button", "00:11:22:33:44:55:66:77-2-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-2-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_battery", }, }, }, @@ -4280,43 +4280,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.samjin_button_77665544_identify", - "sensor.samjin_button_77665544_power", - "sensor.samjin_button_77665544_temperature", - "binary_sensor.samjin_button_77665544_ias_zone", - "sensor.samjin_button_77665544_basic_rssi", - "sensor.samjin_button_77665544_basic_lqi", + "button.samjin_button_identifybutton", + "sensor.samjin_button_battery", + "sensor.samjin_button_temperature", + "binary_sensor.samjin_button_iaszone", + "sensor.samjin_button_rssi", + "sensor.samjin_button_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_button_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_button_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_button_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.samjin_button_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_lqi", }, }, }, @@ -4336,43 +4336,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.samjin_multi_77665544_identify", - "sensor.samjin_multi_77665544_power", - "sensor.samjin_multi_77665544_temperature", - "binary_sensor.samjin_multi_77665544_ias_zone", - "sensor.samjin_multi_77665544_basic_rssi", - "sensor.samjin_multi_77665544_basic_lqi", + "button.samjin_multi_identifybutton", + "sensor.samjin_multi_battery", + "sensor.samjin_multi_temperature", + "binary_sensor.samjin_multi_iaszone", + "sensor.samjin_multi_rssi", + "sensor.samjin_multi_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_multi_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.samjin_multi_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_lqi", }, }, }, @@ -4392,43 +4392,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.samjin_water_77665544_identify", - "sensor.samjin_water_77665544_power", - "sensor.samjin_water_77665544_temperature", - "binary_sensor.samjin_water_77665544_ias_zone", - "sensor.samjin_water_77665544_basic_rssi", - "sensor.samjin_water_77665544_basic_lqi", + "button.samjin_water_identifybutton", + "sensor.samjin_water_battery", + "sensor.samjin_water_temperature", + "binary_sensor.samjin_water_iaszone", + "sensor.samjin_water_rssi", + "sensor.samjin_water_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_water_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_water_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_water_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.samjin_water_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_lqi", }, }, }, @@ -4448,67 +4448,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.securifi_ltd_unk_model_77665544_identify", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_apparent_power", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_ac_frequency", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_power_factor", - "switch.securifi_ltd_unk_model_77665544_on_off", - "sensor.securifi_ltd_unk_model_77665544_basic_rssi", - "sensor.securifi_ltd_unk_model_77665544_basic_lqi", + "button.securifi_ltd_unk_model_identifybutton", + "sensor.securifi_ltd_unk_model_electricalmeasurement", + "sensor.securifi_ltd_unk_model_electricalmeasurementapparentpower", + "sensor.securifi_ltd_unk_model_electricalmeasurementrmscurrent", + "sensor.securifi_ltd_unk_model_electricalmeasurementrmsvoltage", + "sensor.securifi_ltd_unk_model_electricalmeasurementfrequency", + "sensor.securifi_ltd_unk_model_electricalmeasurementpowerfactor", + "switch.securifi_ltd_unk_model_switch", + "sensor.securifi_ltd_unk_model_rssi", + "sensor.securifi_ltd_unk_model_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_switch", }, }, }, @@ -4528,43 +4528,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sercomm_corp_sz_dws04n_sf_77665544_identify", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", - "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_rssi", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_lqi", + "button.sercomm_corp_sz_dws04n_sf_identifybutton", + "sensor.sercomm_corp_sz_dws04n_sf_battery", + "sensor.sercomm_corp_sz_dws04n_sf_temperature", + "binary_sensor.sercomm_corp_sz_dws04n_sf_iaszone", + "sensor.sercomm_corp_sz_dws04n_sf_rssi", + "sensor.sercomm_corp_sz_dws04n_sf_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_dws04n_sf_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_lqi", }, }, }, @@ -4591,79 +4591,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.sercomm_corp_sz_esw01_77665544_identify", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_ac_frequency", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_power_factor", - "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", - "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", - "light.sercomm_corp_sz_esw01_77665544_on_off", - "sensor.sercomm_corp_sz_esw01_77665544_basic_rssi", - "sensor.sercomm_corp_sz_esw01_77665544_basic_lqi", + "button.sercomm_corp_sz_esw01_identifybutton", + "sensor.sercomm_corp_sz_esw01_electricalmeasurement", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementapparentpower", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmscurrent", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmsvoltage", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementfrequency", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementpowerfactor", + "sensor.sercomm_corp_sz_esw01_smartenergymetering", + "sensor.sercomm_corp_sz_esw01_smartenergysummation", + "light.sercomm_corp_sz_esw01_light", + "sensor.sercomm_corp_sz_esw01_rssi", + "sensor.sercomm_corp_sz_esw01_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sercomm_corp_sz_esw01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.sercomm_corp_sz_esw01_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_lqi", }, }, }, @@ -4683,49 +4683,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sercomm_corp_sz_pir04_77665544_identify", - "sensor.sercomm_corp_sz_pir04_77665544_power", - "sensor.sercomm_corp_sz_pir04_77665544_illuminance", - "sensor.sercomm_corp_sz_pir04_77665544_temperature", - "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", - "sensor.sercomm_corp_sz_pir04_77665544_basic_rssi", - "sensor.sercomm_corp_sz_pir04_77665544_basic_lqi", + "button.sercomm_corp_sz_pir04_identifybutton", + "sensor.sercomm_corp_sz_pir04_battery", + "sensor.sercomm_corp_sz_pir04_illuminance", + "sensor.sercomm_corp_sz_pir04_temperature", + "binary_sensor.sercomm_corp_sz_pir04_iaszone", + "sensor.sercomm_corp_sz_pir04_rssi", + "sensor.sercomm_corp_sz_pir04_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_pir04_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_lqi", }, }, }, @@ -4745,67 +4745,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sinope_technologies_rm3250zb_77665544_identify", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_apparent_power", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_ac_frequency", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_power_factor", - "switch.sinope_technologies_rm3250zb_77665544_on_off", - "sensor.sinope_technologies_rm3250zb_77665544_basic_rssi", - "sensor.sinope_technologies_rm3250zb_77665544_basic_lqi", + "button.sinope_technologies_rm3250zb_identifybutton", + "sensor.sinope_technologies_rm3250zb_electricalmeasurement", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementapparentpower", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmscurrent", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmsvoltage", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementfrequency", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementpowerfactor", + "switch.sinope_technologies_rm3250zb_switch", + "sensor.sinope_technologies_rm3250zb_rssi", + "sensor.sinope_technologies_rm3250zb_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_switch", }, }, }, @@ -4832,79 +4832,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sinope_technologies_th1123zb_77665544_identify", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_apparent_power", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_ac_frequency", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_power_factor", - "sensor.sinope_technologies_th1123zb_77665544_temperature", - "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", - "climate.sinope_technologies_th1123zb_77665544_thermostat", - "sensor.sinope_technologies_th1123zb_77665544_basic_rssi", - "sensor.sinope_technologies_th1123zb_77665544_basic_lqi", + "button.sinope_technologies_th1123zb_identifybutton", + "sensor.sinope_technologies_th1123zb_electricalmeasurement", + "sensor.sinope_technologies_th1123zb_electricalmeasurementapparentpower", + "sensor.sinope_technologies_th1123zb_electricalmeasurementrmscurrent", + "sensor.sinope_technologies_th1123zb_electricalmeasurementrmsvoltage", + "sensor.sinope_technologies_th1123zb_electricalmeasurementfrequency", + "sensor.sinope_technologies_th1123zb_electricalmeasurementpowerfactor", + "sensor.sinope_technologies_th1123zb_temperature", + "sensor.sinope_technologies_th1123zb_sinopehvacaction", + "climate.sinope_technologies_th1123zb_thermostat", + "sensor.sinope_technologies_th1123zb_rssi", + "sensor.sinope_technologies_th1123zb_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_identifybutton", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "Thermostat", - DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1123zb_77665544_thermostat", + DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1123zb_thermostat", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_sinopehvacaction", }, }, }, @@ -4931,79 +4931,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sinope_technologies_th1124zb_77665544_identify", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_apparent_power", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_ac_frequency", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_power_factor", - "sensor.sinope_technologies_th1124zb_77665544_temperature", - "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", - "climate.sinope_technologies_th1124zb_77665544_thermostat", - "sensor.sinope_technologies_th1124zb_77665544_basic_rssi", - "sensor.sinope_technologies_th1124zb_77665544_basic_lqi", + "button.sinope_technologies_th1124zb_identifybutton", + "sensor.sinope_technologies_th1124zb_electricalmeasurement", + "sensor.sinope_technologies_th1124zb_electricalmeasurementapparentpower", + "sensor.sinope_technologies_th1124zb_electricalmeasurementrmscurrent", + "sensor.sinope_technologies_th1124zb_electricalmeasurementrmsvoltage", + "sensor.sinope_technologies_th1124zb_electricalmeasurementfrequency", + "sensor.sinope_technologies_th1124zb_electricalmeasurementpowerfactor", + "sensor.sinope_technologies_th1124zb_temperature", + "sensor.sinope_technologies_th1124zb_sinopehvacaction", + "climate.sinope_technologies_th1124zb_thermostat", + "sensor.sinope_technologies_th1124zb_rssi", + "sensor.sinope_technologies_th1124zb_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_identifybutton", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "Thermostat", - DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_77665544_thermostat", + DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_thermostat", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_sinopehvacaction", }, }, }, @@ -5023,73 +5023,73 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.smartthings_outletv4_77665544_identify", - "sensor.smartthings_outletv4_77665544_electrical_measurement", - "sensor.smartthings_outletv4_77665544_electrical_measurement_apparent_power", - "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", - "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", - "sensor.smartthings_outletv4_77665544_electrical_measurement_ac_frequency", - "sensor.smartthings_outletv4_77665544_electrical_measurement_power_factor", - "binary_sensor.smartthings_outletv4_77665544_binary_input", - "switch.smartthings_outletv4_77665544_on_off", - "sensor.smartthings_outletv4_77665544_basic_rssi", - "sensor.smartthings_outletv4_77665544_basic_lqi", + "button.smartthings_outletv4_identifybutton", + "sensor.smartthings_outletv4_electricalmeasurement", + "sensor.smartthings_outletv4_electricalmeasurementapparentpower", + "sensor.smartthings_outletv4_electricalmeasurementrmscurrent", + "sensor.smartthings_outletv4_electricalmeasurementrmsvoltage", + "sensor.smartthings_outletv4_electricalmeasurementfrequency", + "sensor.smartthings_outletv4_electricalmeasurementpowerfactor", + "binary_sensor.smartthings_outletv4_binaryinput", + "switch.smartthings_outletv4_switch", + "sensor.smartthings_outletv4_rssi", + "sensor.smartthings_outletv4_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_outletv4_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_outletv4_binaryinput", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementapparentpower", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementrmscurrent", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_switch", }, }, }, @@ -5109,37 +5109,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.smartthings_tagv4_77665544_identify", - "device_tracker.smartthings_tagv4_77665544_power", - "binary_sensor.smartthings_tagv4_77665544_binary_input", - "sensor.smartthings_tagv4_77665544_basic_rssi", - "sensor.smartthings_tagv4_77665544_basic_lqi", + "button.smartthings_tagv4_identifybutton", + "device_tracker.smartthings_tagv4_devicescanner", + "binary_sensor.smartthings_tagv4_binaryinput", + "sensor.smartthings_tagv4_rssi", + "sensor.smartthings_tagv4_lqi", ], DEV_SIG_ENT_MAP: { ("device_tracker", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "ZHADeviceScannerEntity", - DEV_SIG_ENT_MAP_ID: "device_tracker.smartthings_tagv4_77665544_power", + DEV_SIG_ENT_MAP_ID: "device_tracker.smartthings_tagv4_devicescanner", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_tagv4_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_tagv4_binaryinput", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_lqi", }, }, }, @@ -5159,31 +5159,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.third_reality_inc_3rss007z_77665544_identify", - "switch.third_reality_inc_3rss007z_77665544_on_off", - "sensor.third_reality_inc_3rss007z_77665544_basic_rssi", - "sensor.third_reality_inc_3rss007z_77665544_basic_lqi", + "button.third_reality_inc_3rss007z_identifybutton", + "switch.third_reality_inc_3rss007z_switch", + "sensor.third_reality_inc_3rss007z_rssi", + "sensor.third_reality_inc_3rss007z_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss007z_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss007z_switch", }, }, }, @@ -5203,37 +5203,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.third_reality_inc_3rss008z_77665544_identify", - "sensor.third_reality_inc_3rss008z_77665544_power", - "switch.third_reality_inc_3rss008z_77665544_on_off", - "sensor.third_reality_inc_3rss008z_77665544_basic_rssi", - "sensor.third_reality_inc_3rss008z_77665544_basic_lqi", + "button.third_reality_inc_3rss008z_identifybutton", + "sensor.third_reality_inc_3rss008z_battery", + "switch.third_reality_inc_3rss008z_switch", + "sensor.third_reality_inc_3rss008z_rssi", + "sensor.third_reality_inc_3rss008z_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss008z_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss008z_switch", }, }, }, @@ -5253,43 +5253,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.visonic_mct_340_e_77665544_identify", - "sensor.visonic_mct_340_e_77665544_power", - "sensor.visonic_mct_340_e_77665544_temperature", - "binary_sensor.visonic_mct_340_e_77665544_ias_zone", - "sensor.visonic_mct_340_e_77665544_basic_rssi", - "sensor.visonic_mct_340_e_77665544_basic_lqi", + "button.visonic_mct_340_e_identifybutton", + "sensor.visonic_mct_340_e_battery", + "sensor.visonic_mct_340_e_temperature", + "binary_sensor.visonic_mct_340_e_iaszone", + "sensor.visonic_mct_340_e_rssi", + "sensor.visonic_mct_340_e_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.visonic_mct_340_e_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.visonic_mct_340_e_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_lqi", }, }, }, @@ -5309,43 +5309,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.zen_within_zen_01_77665544_identify", - "sensor.zen_within_zen_01_77665544_power", - "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", - "climate.zen_within_zen_01_77665544_fan_thermostat", - "sensor.zen_within_zen_01_77665544_basic_rssi", - "sensor.zen_within_zen_01_77665544_basic_lqi", + "button.zen_within_zen_01_identifybutton", + "sensor.zen_within_zen_01_battery", + "sensor.zen_within_zen_01_thermostathvacaction", + "climate.zen_within_zen_01_zenwithinthermostat", + "sensor.zen_within_zen_01_rssi", + "sensor.zen_within_zen_01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_identifybutton", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat", "fan"], DEV_SIG_ENT_MAP_CLASS: "ZenWithinThermostat", - DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_77665544_fan_thermostat", + DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_zenwithinthermostat", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_thermostathvacaction", }, }, }, @@ -5386,43 +5386,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", - "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_rssi", - "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_lqi", + "light.tyzb01_ns1ndbww_ts0004_light", + "light.tyzb01_ns1ndbww_ts0004_light_2", + "light.tyzb01_ns1ndbww_ts0004_light_3", + "light.tyzb01_ns1ndbww_ts0004_light_4", + "sensor.tyzb01_ns1ndbww_ts0004_rssi", + "sensor.tyzb01_ns1ndbww_ts0004_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_lqi", }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_2", }, ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_3", }, ("light", "00:11:22:33:44:55:66:77-4"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_4", }, }, }, @@ -5442,37 +5442,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.netvox_z308e3ed_77665544_identify", - "sensor.netvox_z308e3ed_77665544_power", - "binary_sensor.netvox_z308e3ed_77665544_ias_zone", - "sensor.netvox_z308e3ed_77665544_basic_rssi", - "sensor.netvox_z308e3ed_77665544_basic_lqi", + "button.netvox_z308e3ed_identifybutton", + "sensor.netvox_z308e3ed_battery", + "binary_sensor.netvox_z308e3ed_iaszone", + "sensor.netvox_z308e3ed_rssi", + "sensor.netvox_z308e3ed_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.netvox_z308e3ed_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.netvox_z308e3ed_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_lqi", }, }, }, @@ -5492,43 +5492,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sengled_e11_g13_77665544_identify", - "light.sengled_e11_g13_77665544_level_on_off", - "sensor.sengled_e11_g13_77665544_smartenergy_metering", - "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", - "sensor.sengled_e11_g13_77665544_basic_rssi", - "sensor.sengled_e11_g13_77665544_basic_lqi", + "button.sengled_e11_g13_identifybutton", + "light.sengled_e11_g13_light", + "sensor.sengled_e11_g13_smartenergymetering", + "sensor.sengled_e11_g13_smartenergysummation", + "sensor.sengled_e11_g13_rssi", + "sensor.sengled_e11_g13_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_lqi", }, }, }, @@ -5548,43 +5548,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sengled_e12_n14_77665544_identify", - "light.sengled_e12_n14_77665544_level_on_off", - "sensor.sengled_e12_n14_77665544_smartenergy_metering", - "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", - "sensor.sengled_e12_n14_77665544_basic_rssi", - "sensor.sengled_e12_n14_77665544_basic_lqi", + "button.sengled_e12_n14_identifybutton", + "light.sengled_e12_n14_light", + "sensor.sengled_e12_n14_smartenergymetering", + "sensor.sengled_e12_n14_smartenergysummation", + "sensor.sengled_e12_n14_rssi", + "sensor.sengled_e12_n14_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_lqi", }, }, }, @@ -5604,43 +5604,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sengled_z01_a19nae26_77665544_identify", - "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", - "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", - "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", - "sensor.sengled_z01_a19nae26_77665544_basic_rssi", - "sensor.sengled_z01_a19nae26_77665544_basic_lqi", + "button.sengled_z01_a19nae26_identifybutton", + "light.sengled_z01_a19nae26_light", + "sensor.sengled_z01_a19nae26_smartenergymetering", + "sensor.sengled_z01_a19nae26_smartenergysummation", + "sensor.sengled_z01_a19nae26_rssi", + "sensor.sengled_z01_a19nae26_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_lqi", }, }, }, @@ -5660,31 +5660,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.unk_manufacturer_unk_model_77665544_identify", - "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", - "sensor.unk_manufacturer_unk_model_77665544_basic_rssi", - "sensor.unk_manufacturer_unk_model_77665544_basic_lqi", + "button.unk_manufacturer_unk_model_identifybutton", + "cover.unk_manufacturer_unk_model_shade", + "sensor.unk_manufacturer_unk_model_rssi", + "sensor.unk_manufacturer_unk_model_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off", "shade"], DEV_SIG_ENT_MAP_CLASS: "Shade", - DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", + DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_shade", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_lqi", }, }, }, @@ -5809,139 +5809,139 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["232:0x0008"], DEV_SIG_ENTITIES: [ - "number.digi_xbee3_77665544_analog_output", - "number.digi_xbee3_77665544_analog_output_2", - "sensor.digi_xbee3_77665544_analog_input", - "sensor.digi_xbee3_77665544_analog_input_2", - "sensor.digi_xbee3_77665544_analog_input_3", - "sensor.digi_xbee3_77665544_analog_input_4", - "sensor.digi_xbee3_77665544_analog_input_5", - "switch.digi_xbee3_77665544_on_off", - "switch.digi_xbee3_77665544_on_off_2", - "switch.digi_xbee3_77665544_on_off_3", - "switch.digi_xbee3_77665544_on_off_4", - "switch.digi_xbee3_77665544_on_off_5", - "switch.digi_xbee3_77665544_on_off_6", - "switch.digi_xbee3_77665544_on_off_7", - "switch.digi_xbee3_77665544_on_off_8", - "switch.digi_xbee3_77665544_on_off_9", - "switch.digi_xbee3_77665544_on_off_10", - "switch.digi_xbee3_77665544_on_off_11", - "switch.digi_xbee3_77665544_on_off_12", - "switch.digi_xbee3_77665544_on_off_13", - "switch.digi_xbee3_77665544_on_off_14", - "switch.digi_xbee3_77665544_on_off_15", + "number.digi_xbee3_number", + "number.digi_xbee3_number_2", + "sensor.digi_xbee3_analoginput", + "sensor.digi_xbee3_analoginput_2", + "sensor.digi_xbee3_analoginput_3", + "sensor.digi_xbee3_analoginput_4", + "sensor.digi_xbee3_analoginput_5", + "switch.digi_xbee3_switch", + "switch.digi_xbee3_switch_2", + "switch.digi_xbee3_switch_3", + "switch.digi_xbee3_switch_4", + "switch.digi_xbee3_switch_5", + "switch.digi_xbee3_switch_6", + "switch.digi_xbee3_switch_7", + "switch.digi_xbee3_switch_8", + "switch.digi_xbee3_switch_9", + "switch.digi_xbee3_switch_10", + "switch.digi_xbee3_switch_11", + "switch.digi_xbee3_switch_12", + "switch.digi_xbee3_switch_13", + "switch.digi_xbee3_switch_14", + "switch.digi_xbee3_switch_15", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-208-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput", }, ("switch", "00:11:22:33:44:55:66:77-208-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch", }, ("sensor", "00:11:22:33:44:55:66:77-209-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_2", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_2", }, ("switch", "00:11:22:33:44:55:66:77-209-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_2", }, ("sensor", "00:11:22:33:44:55:66:77-210-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_3", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_3", }, ("switch", "00:11:22:33:44:55:66:77-210-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_3", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_3", }, ("sensor", "00:11:22:33:44:55:66:77-211-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_4", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_4", }, ("switch", "00:11:22:33:44:55:66:77-211-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_4", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_4", }, ("switch", "00:11:22:33:44:55:66:77-212-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_5", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_5", }, ("switch", "00:11:22:33:44:55:66:77-213-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_6", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_6", }, ("switch", "00:11:22:33:44:55:66:77-214-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_7", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_7", }, ("sensor", "00:11:22:33:44:55:66:77-215-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_5", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_5", }, ("switch", "00:11:22:33:44:55:66:77-215-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_8", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_8", }, ("switch", "00:11:22:33:44:55:66:77-216-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_9", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_9", }, ("switch", "00:11:22:33:44:55:66:77-217-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_10", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_10", }, ("number", "00:11:22:33:44:55:66:77-218-13"): { DEV_SIG_CHANNELS: ["analog_output"], DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output", + DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_number", }, ("switch", "00:11:22:33:44:55:66:77-218-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_11", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_11", }, ("switch", "00:11:22:33:44:55:66:77-219-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_12", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_12", }, ("number", "00:11:22:33:44:55:66:77-219-13"): { DEV_SIG_CHANNELS: ["analog_output"], DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output_2", + DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_number_2", }, ("switch", "00:11:22:33:44:55:66:77-220-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_13", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_13", }, ("switch", "00:11:22:33:44:55:66:77-221-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_14", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_14", }, ("switch", "00:11:22:33:44:55:66:77-222-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_15", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_15", }, }, }, @@ -5961,37 +5961,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "sensor.efektalab_ru_efekta_pws_77665544_power", - "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", - "sensor.efektalab_ru_efekta_pws_77665544_temperature", - "sensor.efektalab_ru_efekta_pws_77665544_basic_rssi", - "sensor.efektalab_ru_efekta_pws_77665544_basic_lqi", + "sensor.efektalab_ru_efekta_pws_battery", + "sensor.efektalab_ru_efekta_pws_soilmoisture", + "sensor.efektalab_ru_efekta_pws_temperature", + "sensor.efektalab_ru_efekta_pws_rssi", + "sensor.efektalab_ru_efekta_pws_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1032"): { DEV_SIG_CHANNELS: ["soil_moisture"], DEV_SIG_ENT_MAP_CLASS: "SoilMoisture", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_soilmoisture", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_lqi", }, }, }, diff --git a/tests/components/zwave_me/test_config_flow.py b/tests/components/zwave_me/test_config_flow.py index 36a73c4fc06..57ead9a9e60 100644 --- a/tests/components/zwave_me/test_config_flow.py +++ b/tests/components/zwave_me/test_config_flow.py @@ -5,12 +5,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.zwave_me.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, - FlowResult, -) +from homeassistant.data_entry_flow import FlowResult, FlowResultType from tests.common import MockConfigEntry @@ -42,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -53,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -76,7 +71,7 @@ async def test_zeroconf(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -87,7 +82,7 @@ async def test_zeroconf(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -104,7 +99,7 @@ async def test_error_handling_zeroconf(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_valid_uuid_set" @@ -114,7 +109,7 @@ async def test_handle_error_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -145,7 +140,7 @@ async def test_duplicate_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -154,7 +149,7 @@ async def test_duplicate_user(hass: HomeAssistant): "token": "test-token", }, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -181,5 +176,5 @@ async def test_duplicate_zeroconf(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/conftest.py b/tests/conftest.py index 97b1a959d2c..50c24df8d44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,7 @@ from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password from homeassistant.components import mqtt, recorder +from homeassistant.components.network.models import Adapter, IPv4ConfiguredAddress from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, @@ -30,7 +31,7 @@ from homeassistant.components.websocket_api.auth import ( from homeassistant.components.websocket_api.http import URL from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers import config_entry_oauth2_flow, recorder as recorder_helper from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, location @@ -166,7 +167,8 @@ def verify_cleanup(): pytest.exit(f"Detected non stopped instances ({count}), aborting test run") threads = frozenset(threading.enumerate()) - threads_before - assert not threads + for thread in threads: + assert isinstance(thread, threading._DummyThread) @pytest.fixture(autouse=True) @@ -499,7 +501,7 @@ def fail_on_log_exception(request, monkeypatch): @pytest.fixture -def mqtt_config(): +def mqtt_config_entry_data(): """Fixture to allow overriding MQTT config.""" return None @@ -551,7 +553,7 @@ def mqtt_client_mock(hass): async def mqtt_mock( hass, mqtt_client_mock, - mqtt_config, + mqtt_config_entry_data, mqtt_mock_entry_no_yaml_config, ): """Fixture to mock MQTT component.""" @@ -559,15 +561,18 @@ async def mqtt_mock( @asynccontextmanager -async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config): +async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config_entry_data): """Fixture to mock a delayed setup of the MQTT config entry.""" - if mqtt_config is None: - mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}} + if mqtt_config_entry_data is None: + mqtt_config_entry_data = { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: {}, + } await hass.async_block_till_done() entry = MockConfigEntry( - data=mqtt_config, + data=mqtt_config_entry_data, domain=mqtt.DOMAIN, title="MQTT", ) @@ -611,7 +616,9 @@ async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config): @pytest.fixture -async def mqtt_mock_entry_no_yaml_config(hass, mqtt_client_mock, mqtt_config): +async def mqtt_mock_entry_no_yaml_config( + hass, mqtt_client_mock, mqtt_config_entry_data +): """Set up an MQTT config entry without MQTT yaml config.""" async def _async_setup_config_entry(hass, entry): @@ -624,12 +631,16 @@ async def mqtt_mock_entry_no_yaml_config(hass, mqtt_client_mock, mqtt_config): """Set up the MQTT config entry.""" return await mqtt_mock_entry(_async_setup_config_entry) - async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + async with _mqtt_mock_entry( + hass, mqtt_client_mock, mqtt_config_entry_data + ) as mqtt_mock_entry: yield _setup_mqtt_entry @pytest.fixture -async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): +async def mqtt_mock_entry_with_yaml_config( + hass, mqtt_client_mock, mqtt_config_entry_data +): """Set up an MQTT config entry with MQTT yaml config.""" async def _async_do_not_setup_config_entry(hass, entry): @@ -640,10 +651,31 @@ async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): """Set up the MQTT config entry.""" return await mqtt_mock_entry(_async_do_not_setup_config_entry) - async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + async with _mqtt_mock_entry( + hass, mqtt_client_mock, mqtt_config_entry_data + ) as mqtt_mock_entry: yield _setup_mqtt_entry +@pytest.fixture(autouse=True) +def mock_network(): + """Mock network.""" + mock_adapter = Adapter( + name="eth0", + index=0, + enabled=True, + auto=True, + default=True, + ipv4=[IPv4ConfiguredAddress(address="10.10.10.10", network_prefix=24)], + ipv6=[], + ) + with patch( + "homeassistant.components.network.network.async_load_adapters", + return_value=[mock_adapter], + ): + yield + + @pytest.fixture(autouse=True) def mock_get_source_ip(): """Mock network util's async_get_source_ip.""" @@ -758,6 +790,8 @@ async def _async_init_recorder_component(hass, add_config=None): with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration.migrate_schema" ): + if recorder.DOMAIN not in hass.data: + recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( hass, recorder.DOMAIN, {recorder.DOMAIN: config} ) @@ -835,3 +869,54 @@ def mock_integration_frame(): ], ): yield correct_frame + + +@pytest.fixture(name="enable_bluetooth") +async def mock_enable_bluetooth( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): + """Fixture to mock starting the bleak scanner.""" + entry = MockConfigEntry(domain="bluetooth") + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +@pytest.fixture(name="mock_bluetooth_adapters") +def mock_bluetooth_adapters(): + """Fixture to mock bluetooth adapters.""" + with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=[]): + yield + + +@pytest.fixture(name="mock_bleak_scanner_start") +def mock_bleak_scanner_start(): + """Fixture to mock starting the bleak scanner.""" + + # Late imports to avoid loading bleak unless we need it + + import bleak # pylint: disable=import-outside-toplevel + + from homeassistant.components.bluetooth import ( # pylint: disable=import-outside-toplevel + models as bluetooth_models, + ) + + scanner = bleak.BleakScanner + bluetooth_models.HA_BLEAK_SCANNER = None + + with patch("homeassistant.components.bluetooth.HaBleakScanner.stop"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ) as mock_bleak_scanner_start: + yield mock_bleak_scanner_start + + # We need to drop the stop method from the object since we patched + # out start and this fixture will expire before the stop method is called + # when EVENT_HOMEASSISTANT_STOP is fired. + if bluetooth_models.HA_BLEAK_SCANNER: + bluetooth_models.HA_BLEAK_SCANNER.stop = AsyncMock() + bleak.BleakScanner = scanner + + +@pytest.fixture(name="mock_bluetooth") +def mock_bluetooth(mock_bleak_scanner_start, mock_bluetooth_adapters): + """Mock out bluetooth from starting.""" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 2c125d9fada..b0037aa3800 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -6631,6 +6631,100 @@ "serializedGlobalTradeItemNumber": "3014F7110000000000056775", "type": "FULL_FLUSH_CONTACT_INTERFACE_6", "updateState": "UP_TO_DATE" + }, + "3014F7110000000000STE2015": { + "availableFirmwareVersion": "1.0.26", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.18", + "firmwareVersionInteger": 65554, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000STE2015", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000024"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -60, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000STE2015", + "functionalChannelType": "TEMPERATURE_SENSOR_2_EXTERNAL_DELTA_CHANNEL", + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000025"], + "index": 1, + "label": "", + "temperatureExternalDelta": -0.9, + "temperatureExternalOne": 24.5, + "temperatureExternalTwo": 25.4 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000STE2015", + "label": "STE2", + "lastStatusUpdate": 1645012379988, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 415, + "modelType": "HmIP-STE2-PCB", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000STE2015", + "type": "TEMPERATURE_SENSOR_2_EXTERNAL_DELTA", + "updateState": "TRANSFERING_UPDATE" } }, "groups": { diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 1ffb4267167..7bac31e8e19 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -183,7 +183,7 @@ async def test_warning_close_session_custom(hass, caplog): await session.close() assert ( "Detected integration that closes the Home Assistant aiohttp session. " - "Please report issue to the custom component author for hue using this method at " + "Please report issue to the custom integration author for hue using this method at " "custom_components/hue/light.py, line 23: await session.close()" in caplog.text ) diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 979aa8bf088..a4a3e2b27e7 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -51,7 +51,7 @@ async def test_single_entry_allowed(hass, discovery_flow_conf): MockConfigEntry(domain="test").add_to_hass(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -62,7 +62,7 @@ async def test_user_no_devices_found(hass, discovery_flow_conf): flow.context = {"source": config_entries.SOURCE_USER} result = await flow.async_step_confirm(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -75,7 +75,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): "test", context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" progress = hass.config_entries.flow.async_progress() @@ -88,12 +88,13 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): } result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( "source", [ + config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, @@ -110,13 +111,14 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): MockConfigEntry(domain="test").add_to_hass(hass) result = await getattr(flow, f"async_step_{source}")({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @pytest.mark.parametrize( "source", [ + config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, @@ -132,16 +134,17 @@ async def test_discovery_confirmation(hass, discovery_flow_conf, source): result = await getattr(flow, f"async_step_{source}")({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await flow.async_step_confirm({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( "source", [ + config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, @@ -160,7 +163,7 @@ async def test_discovery_during_onboarding(hass, discovery_flow_conf, source): ): result = await getattr(flow, f"async_step_{source}")({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_multiple_discoveries(hass, discovery_flow_conf): @@ -170,13 +173,13 @@ async def test_multiple_discoveries(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Second discovery result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_only_one_in_progress(hass, discovery_flow_conf): @@ -187,21 +190,21 @@ async def test_only_one_in_progress(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # User starts flow result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Discovery flow has not been aborted assert len(hass.config_entries.flow.async_progress()) == 2 # Discovery should be aborted once user confirms result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -213,14 +216,14 @@ async def test_import_abort_discovery(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Start import flow result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_IMPORT}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Discovery flow has been aborted assert len(hass.config_entries.flow.async_progress()) == 0 @@ -234,7 +237,7 @@ async def test_import_no_confirmation(hass, discovery_flow_conf): discovery_flow_conf["discovered"] = True result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_import_single_instance(hass, discovery_flow_conf): @@ -246,7 +249,7 @@ async def test_import_single_instance(hass, discovery_flow_conf): MockConfigEntry(domain="test").add_to_hass(hass) result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_ignored_discoveries(hass, discovery_flow_conf): @@ -256,7 +259,7 @@ async def test_ignored_discoveries(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM flow = next( ( @@ -278,7 +281,7 @@ async def test_ignored_discoveries(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_webhook_single_entry_allowed(hass, webhook_flow_conf): @@ -289,7 +292,7 @@ async def test_webhook_single_entry_allowed(hass, webhook_flow_conf): MockConfigEntry(domain="test_single").add_to_hass(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -302,7 +305,7 @@ async def test_webhook_multiple_entries_allowed(hass, webhook_flow_conf): hass.config.api = Mock(base_url="http://example.com") result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): @@ -316,7 +319,7 @@ async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): ) result = await flow.async_step_user(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["webhook_id"] is not None @@ -341,7 +344,7 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): result = await hass.config_entries.flow.async_init( "test_single", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "hass_nabucasa.cloudhooks.Cloudhooks.async_create", @@ -358,7 +361,7 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["description_placeholders"]["webhook_url"] == "https://example.com" assert len(mock_create.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 @@ -395,7 +398,7 @@ async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_ result = await hass.config_entries.flow.async_init( "test_single", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "hass_nabucasa.cloudhooks.Cloudhooks.async_create", @@ -413,16 +416,5 @@ async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_ result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" - - -async def test_warning_deprecated_connection_class(hass, caplog): - """Test that we log a warning when the connection_class is used.""" - discovery_function = Mock() - with patch.dict(config_entries.HANDLERS): - config_entry_flow.register_discovery_flow( - "test", "Test", discovery_function, connection_class="local_polling" - ) - - assert "integration is setting a connection_class" in caplog.text diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index e5d220c55df..652ce69e57d 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -110,7 +110,7 @@ async def test_abort_if_no_implementation(hass, flow_handler): flow = flow_handler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -121,7 +121,7 @@ async def test_missing_credentials_for_domain(hass, flow_handler): with patch("homeassistant.loader.APPLICATION_CREDENTIALS", [TEST_DOMAIN]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -139,7 +139,7 @@ async def test_abort_if_authorization_timeout( ): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -157,7 +157,7 @@ async def test_abort_if_no_url_available( ): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_url_available" @@ -179,7 +179,7 @@ async def test_abort_if_oauth_error( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # Pick implementation @@ -195,7 +195,7 @@ async def test_abort_if_oauth_error( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -219,7 +219,7 @@ async def test_abort_if_oauth_error( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "oauth_error" @@ -241,7 +241,7 @@ async def test_abort_if_oauth_rejected( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # Pick implementation @@ -257,7 +257,7 @@ async def test_abort_if_oauth_rejected( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -273,7 +273,7 @@ async def test_abort_if_oauth_rejected( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "user_rejected_authorize" assert result["description_placeholders"] == {"error": "access_denied"} @@ -291,7 +291,7 @@ async def test_step_discovery(hass, flow_handler, local_impl): data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" @@ -308,7 +308,7 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" result = await hass.config_entries.flow.async_init( @@ -317,7 +317,7 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -340,7 +340,7 @@ async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl) data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -362,7 +362,7 @@ async def test_full_flow( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # Pick implementation @@ -378,7 +378,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index b9067a3db1c..698d3cfe98a 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -715,7 +715,7 @@ async def test_warn_slow_write_state_custom_component(hass, caplog): assert ( "Updating state for comp_test.test_entity " "(.CustomComponentEntity'>) " - "took 10.000 seconds. Please report it to the custom component author." + "took 10.000 seconds. Please report it to the custom integration author." ) in caplog.text diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 80a37f9f2fd..d7f77eeacda 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1194,6 +1194,7 @@ async def test_entity_info_added_to_entity_registry(hass): capability_attributes={"max": 100}, device_class="mock-device-class", entity_category=EntityCategory.CONFIG, + has_entity_name=True, icon="nice:icon", name="best name", supported_features=5, @@ -1213,6 +1214,7 @@ async def test_entity_info_added_to_entity_registry(hass): capabilities={"max": 100}, device_class=None, entity_category=EntityCategory.CONFIG, + has_entity_name=True, icon=None, id=ANY, name=None, diff --git a/tests/helpers/test_helper_config_entry_flow.py b/tests/helpers/test_helper_config_entry_flow.py index 46e8998c738..2967b202efe 100644 --- a/tests/helpers/test_helper_config_entry_flow.py +++ b/tests/helpers/test_helper_config_entry_flow.py @@ -38,7 +38,7 @@ def manager(): async def async_finish_flow(self, flow, result): """Test finish flow.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: result["source"] = flow.context.get("source") entries.append(result) return result @@ -110,11 +110,11 @@ async def test_config_flow_advanced_option( # Start flow in basic mode result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == ["option1"] result = await manager.async_configure(result["flow_id"], {"option1": "blabla"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert result["options"] == { "advanced_default": "a very reasonable default", @@ -126,7 +126,7 @@ async def test_config_flow_advanced_option( # Start flow in advanced mode result = await manager.async_init("test", context={"show_advanced_options": True}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -136,7 +136,7 @@ async def test_config_flow_advanced_option( result = await manager.async_configure( result["flow_id"], {"advanced_no_default": "abc123", "option1": "blabla"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert result["options"] == { "advanced_default": "a very reasonable default", @@ -149,7 +149,7 @@ async def test_config_flow_advanced_option( # Start flow in advanced mode result = await manager.async_init("test", context={"show_advanced_options": True}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -164,7 +164,7 @@ async def test_config_flow_advanced_option( "option1": "blabla", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert result["options"] == { "advanced_default": "not default", @@ -216,13 +216,13 @@ async def test_options_flow_advanced_option( # Start flow in basic mode result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == ["option1"] result = await hass.config_entries.options.async_configure( result["flow_id"], {"option1": "blublu"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "advanced_default": "not default", "advanced_no_default": "abc123", @@ -236,7 +236,7 @@ async def test_options_flow_advanced_option( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -246,7 +246,7 @@ async def test_options_flow_advanced_option( result = await hass.config_entries.options.async_configure( result["flow_id"], {"advanced_no_default": "def456", "option1": "blabla"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "advanced_default": "a very reasonable default", "advanced_no_default": "def456", @@ -260,7 +260,7 @@ async def test_options_flow_advanced_option( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -275,7 +275,7 @@ async def test_options_flow_advanced_option( "option1": "blabla", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "advanced_default": "also not default", "advanced_no_default": "abc123", diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index cdb650f7686..068c0fe1470 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -153,6 +153,6 @@ async def test_warning_close_session_custom(hass, caplog): await httpx_session.aclose() assert ( "Detected integration that closes the Home Assistant httpx client. " - "Please report issue to the custom component author for hue using this method at " + "Please report issue to the custom integration author for hue using this method at " "custom_components/hue/light.py, line 23: await session.aclose()" in caplog.text ) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 2e30a649a7b..d993233ac5d 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -135,8 +135,8 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): "homeassistant.helpers.translation.load_translations_files", return_value={"component1": {"title": "world"}}, ), patch( - "homeassistant.helpers.translation.async_get_integration", - return_value=integration, + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component1": integration}, ): translations = await translation.async_get_translations( hass, "en", "title", config_flow=True @@ -164,8 +164,8 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): "homeassistant.helpers.translation.load_translations_files", return_value={"component2": {"title": "world"}}, ), patch( - "homeassistant.helpers.translation.async_get_integration", - return_value=integration, + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component2": integration}, ): translations = await translation.async_get_translations( hass, "en", "title", config_flow=True @@ -212,8 +212,8 @@ async def test_get_translations_while_loading_components(hass): "homeassistant.helpers.translation.load_translations_files", mock_load_translation_files, ), patch( - "homeassistant.helpers.translation.async_get_integration", - return_value=integration, + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component1": integration}, ): tasks = [ translation.async_get_translations(hass, "en", "title") for _ in range(5) diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 0d0970a4756..4e5f07a2232 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -402,3 +402,32 @@ async def test_not_schedule_refresh_if_system_option_disable_polling(hass): crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) crd.async_add_listener(lambda: None) assert crd._unsub_refresh is None + + +async def test_async_set_update_error(crd, caplog): + """Test manually setting an update failure.""" + update_callback = Mock() + crd.async_add_listener(update_callback) + + crd.async_set_update_error(aiohttp.ClientError("Client Failure #1")) + assert crd.last_update_success is False + assert "Client Failure #1" in caplog.text + update_callback.assert_called_once() + update_callback.reset_mock() + + # Additional failure does not log or change state + crd.async_set_update_error(aiohttp.ClientError("Client Failure #2")) + assert crd.last_update_success is False + assert "Client Failure #2" not in caplog.text + update_callback.assert_not_called() + update_callback.reset_mock() + + crd.async_set_updated_data(200) + assert crd.last_update_success is True + update_callback.assert_called_once() + update_callback.reset_mock() + + crd.async_set_update_error(aiohttp.ClientError("Client Failure #3")) + assert crd.last_update_success is False + assert "Client Failure #2" not in caplog.text + update_callback.assert_called_once() diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 8b4b8d4d058..e549d21fe0f 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -40,53 +40,34 @@ def test_regex_get_module_platform( @pytest.mark.parametrize( - ("string", "expected_x", "expected_y", "expected_z", "expected_a"), + ("string", "expected_count", "expected_items"), [ - ("list[dict[str, str]]", "list", "dict", "str", "str"), - ("list[dict[str, Any]]", "list", "dict", "str", "Any"), + ("Callable[..., None]", 2, ("Callable", "...", "None")), + ("Callable[..., Awaitable[None]]", 2, ("Callable", "...", "Awaitable[None]")), + ("tuple[int, int, int, int]", 4, ("tuple", "int", "int", "int", "int")), + ( + "tuple[int, int, int, int, int]", + 5, + ("tuple", "int", "int", "int", "int", "int"), + ), + ("Awaitable[None]", 1, ("Awaitable", "None")), + ("list[dict[str, str]]", 1, ("list", "dict[str, str]")), + ("list[dict[str, Any]]", 1, ("list", "dict[str, Any]")), ], ) -def test_regex_x_of_y_of_z_comma_a( +def test_regex_x_of_y_i( hass_enforce_type_hints: ModuleType, string: str, - expected_x: str, - expected_y: str, - expected_z: str, - expected_a: str, + expected_count: int, + expected_items: tuple[str, ...], ) -> None: - """Test x_of_y_of_z_comma_a regexes.""" + """Test x_of_y_i regexes.""" matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS - assert (match := matchers["x_of_y_of_z_comma_a"].match(string)) + assert (match := matchers[f"x_of_y_{expected_count}"].match(string)) assert match.group(0) == string - assert match.group(1) == expected_x - assert match.group(2) == expected_y - assert match.group(3) == expected_z - assert match.group(4) == expected_a - - -@pytest.mark.parametrize( - ("string", "expected_x", "expected_y", "expected_z"), - [ - ("Callable[..., None]", "Callable", "...", "None"), - ("Callable[..., Awaitable[None]]", "Callable", "...", "Awaitable[None]"), - ], -) -def test_regex_x_of_y_comma_z( - hass_enforce_type_hints: ModuleType, - string: str, - expected_x: str, - expected_y: str, - expected_z: str, -) -> None: - """Test x_of_y_comma_z regexes.""" - matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS - - assert (match := matchers["x_of_y_comma_z"].match(string)) - assert match.group(0) == string - assert match.group(1) == expected_x - assert match.group(2) == expected_y - assert match.group(3) == expected_z + for index in range(expected_count): + assert match.group(index + 1) == expected_items[index] @pytest.mark.parametrize( @@ -120,6 +101,9 @@ def test_ignore_no_annotations( hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str ) -> None: """Ensure that _is_valid_type is not run if there are no annotations.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = True + func_node = astroid.extract_node( code, "homeassistant.components.pylint_test", @@ -539,6 +523,9 @@ def test_ignore_invalid_entity_properties( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Check invalid entity properties are ignored by default.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = True + class_node = astroid.extract_node( """ class LockEntity(): @@ -737,3 +724,105 @@ def test_valid_mapping_return_type( with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_valid_long_tuple( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check invalid entity properties are ignored by default.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, _, _ = astroid.extract_node( + """ + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class LightEntity(ToggleEntity): + pass + + class TestLight( #@ + LightEntity + ): + @property + def rgbw_color( #@ + self + ) -> tuple[int, int, int, int]: + pass + + @property + def rgbww_color( #@ + self + ) -> tuple[int, int, int, int, int]: + pass + """, + "homeassistant.components.pylint_test.light", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) + + +def test_invalid_long_tuple( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check invalid entity properties are ignored by default.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, rgbw_node, rgbww_node = astroid.extract_node( + """ + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class LightEntity(ToggleEntity): + pass + + class TestLight( #@ + LightEntity + ): + @property + def rgbw_color( #@ + self + ) -> tuple[int, int, int, int, int]: + pass + + @property + def rgbww_color( #@ + self + ) -> tuple[int, int, int, int, float]: + pass + """, + "homeassistant.components.pylint_test.light", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=rgbw_node, + args=["tuple[int, int, int, int]", None], + line=15, + col_offset=4, + end_line=15, + end_col_offset=18, + ), + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=rgbww_node, + args=["tuple[int, int, int, int, int]", None], + line=21, + col_offset=4, + end_line=21, + end_col_offset=19, + ), + ): + type_hint_checker.visit_classdef(class_node) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 232d8fb6bbf..06f800af7f3 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -211,6 +211,82 @@ async def test_setup_after_deps_in_stage_1_ignored(hass): assert order == ["cloud", "an_after_dep", "normal_integration"] +@pytest.mark.parametrize("load_registries", [False]) +async def test_setup_frontend_before_recorder(hass): + """Test frontend is setup before recorder.""" + order = [] + + def gen_domain_setup(domain): + async def async_setup(hass, config): + order.append(domain) + return True + + return async_setup + + mock_integration( + hass, + MockModule( + domain="normal_integration", + async_setup=gen_domain_setup("normal_integration"), + partial_manifest={"after_dependencies": ["an_after_dep"]}, + ), + ) + mock_integration( + hass, + MockModule( + domain="an_after_dep", + async_setup=gen_domain_setup("an_after_dep"), + ), + ) + mock_integration( + hass, + MockModule( + domain="frontend", + async_setup=gen_domain_setup("frontend"), + partial_manifest={ + "dependencies": ["http"], + "after_dependencies": ["an_after_dep"], + }, + ), + ) + mock_integration( + hass, + MockModule( + domain="http", + async_setup=gen_domain_setup("http"), + ), + ) + mock_integration( + hass, + MockModule( + domain="recorder", + async_setup=gen_domain_setup("recorder"), + ), + ) + + await bootstrap._async_set_up_integrations( + hass, + { + "frontend": {}, + "http": {}, + "recorder": {}, + "normal_integration": {}, + "an_after_dep": {}, + }, + ) + + assert "frontend" in hass.config.components + assert "normal_integration" in hass.config.components + assert "recorder" in hass.config.components + assert order == [ + "http", + "frontend", + "recorder", + "an_after_dep", + "normal_integration", + ] + + @pytest.mark.parametrize("load_registries", [False]) async def test_setup_after_deps_via_platform(hass): """Test after_dependencies set up via platform.""" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 9372a906f71..b923e37b636 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -17,7 +17,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo, FlowResult +from homeassistant.data_entry_flow import BaseServiceInfo, FlowResult, FlowResultType from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, @@ -713,14 +713,14 @@ async def test_discovery_notification(hass): ) flow1 = await hass.config_entries.flow.async_configure(flow1["flow_id"], {}) - assert flow1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_discovery") assert state is not None flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) - assert flow2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_discovery") @@ -780,14 +780,14 @@ async def test_reauth_notification(hass): ) flow1 = await hass.config_entries.flow.async_configure(flow1["flow_id"], {}) - assert flow1["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow1["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_reconfigure") assert state is not None flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) - assert flow2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow2["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_reconfigure") @@ -1059,7 +1059,7 @@ async def test_entry_options(hass, manager): await manager.options.async_finish_flow( flow, - {"data": {"second": True}, "type": data_entry_flow.RESULT_TYPE_CREATE_ENTRY}, + {"data": {"second": True}, "type": data_entry_flow.FlowResultType.CREATE_ENTRY}, ) assert entry.data == {"first": True} @@ -1092,7 +1092,7 @@ async def test_entry_options_abort(hass, manager): flow.handler = entry.entry_id # Used to keep reference to config entry assert await manager.options.async_finish_flow( - flow, {"type": data_entry_flow.RESULT_TYPE_ABORT, "reason": "test"} + flow, {"type": data_entry_flow.FlowResultType.ABORT, "reason": "test"} ) @@ -1574,7 +1574,7 @@ async def test_unique_id_existing_entry(hass, manager): "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries("comp") assert len(entries) == 1 @@ -1659,7 +1659,7 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" @@ -1704,7 +1704,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" @@ -1721,7 +1721,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "2.2.2.2" assert entry.data["additional"] == "data" @@ -1773,7 +1773,7 @@ async def test_unique_id_from_discovery_in_setup_retry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(async_reload.mock_calls) == 0 @@ -1792,7 +1792,7 @@ async def test_unique_id_from_discovery_in_setup_retry(hass, manager): ) await hass.async_block_till_done() - assert discovery_result["type"] == RESULT_TYPE_ABORT + assert discovery_result["type"] == FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" assert len(async_reload.mock_calls) == 1 @@ -1833,7 +1833,7 @@ async def test_unique_id_not_update_existing_entry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "0.0.0.0" assert entry.data["additional"] == "data" @@ -1860,14 +1860,14 @@ async def test_unique_id_in_progress(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Will be canceled result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -1898,14 +1898,14 @@ async def test_finish_flow_aborts_progress(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Will finish and cancel other one. result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER}, data={} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -1931,7 +1931,7 @@ async def test_unique_id_ignore(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await manager.flow.async_init( "comp", @@ -1939,7 +1939,7 @@ async def test_unique_id_ignore(hass, manager): data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # assert len(hass.config_entries.flow.async_progress()) == 0 @@ -1992,7 +1992,7 @@ async def test_manual_add_overrides_ignored_entry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" assert len(async_reload.mock_calls) == 0 @@ -2169,7 +2169,7 @@ async def test_unignore_step_form(hass, manager): context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries("comp")[0] assert entry.source == "ignore" @@ -2214,7 +2214,7 @@ async def test_unignore_create_entry(hass, manager): context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries("comp")[0] assert entry.source == "ignore" @@ -2256,7 +2256,7 @@ async def test_unignore_default_impl(hass, manager): context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries("comp")[0] assert entry.source == "ignore" @@ -2321,7 +2321,7 @@ async def test_partial_flows_hidden(hass, manager): # When it's complete it should now be visible in async_progress and have triggered # discovery notifications result = await init_task - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert len(hass.config_entries.flow.async_progress()) == 1 await hass.async_block_till_done() @@ -2497,6 +2497,7 @@ async def test_async_setup_update_entry(hass): @pytest.mark.parametrize( "discovery_source", ( + (config_entries.SOURCE_BLUETOOTH, BaseServiceInfo()), (config_entries.SOURCE_DISCOVERY, {}), (config_entries.SOURCE_SSDP, BaseServiceInfo()), (config_entries.SOURCE_USB, BaseServiceInfo()), @@ -2531,7 +2532,7 @@ async def test_flow_with_default_discovery(hass, manager, discovery_source): result = await manager.flow.async_init( "comp", context={"source": discovery_source[0]}, data=discovery_source[1] ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2544,7 +2545,7 @@ async def test_flow_with_default_discovery(hass, manager, discovery_source): result2 = await manager.flow.async_configure( result["flow_id"], user_input={"fake": "data"} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -2575,7 +2576,7 @@ async def test_flow_with_default_discovery_with_unique_id(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2600,7 +2601,7 @@ async def test_default_discovery_abort_existing_entries(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -2626,13 +2627,13 @@ async def test_default_discovery_in_progress(hass, manager): context={"source": config_entries.SOURCE_DISCOVERY}, data={"unique_id": "mock-unique-id"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Second discovery without a unique ID result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2660,7 +2661,7 @@ async def test_default_discovery_abort_on_new_unique_flow(hass, manager): result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM # Second discovery brings in a unique ID result = await manager.flow.async_init( @@ -2668,7 +2669,7 @@ async def test_default_discovery_abort_on_new_unique_flow(hass, manager): context={"source": config_entries.SOURCE_DISCOVERY}, data={"unique_id": "mock-unique-id"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Ensure the first one is cancelled and we end up with just the last one flows = hass.config_entries.flow.async_progress() @@ -2702,7 +2703,7 @@ async def test_default_discovery_abort_on_user_flow_complete(hass, manager): flow1 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert flow1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow1["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2711,14 +2712,14 @@ async def test_default_discovery_abort_on_user_flow_complete(hass, manager): flow2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert flow2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow2["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 2 # Complete the manual flow result = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Ensure the first flow is gone now flows = hass.config_entries.flow.async_progress() @@ -2780,7 +2781,7 @@ async def test_flow_same_device_multiple_sources(hass, manager): result2 = await manager.flow.async_configure( flows[0]["flow_id"], user_input={"fake": "data"} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -3100,7 +3101,7 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == reason @@ -3250,7 +3251,7 @@ async def test_unique_id_update_while_setup_in_progress( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" @@ -3272,3 +3273,38 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): with pytest.raises(config_entries.OperationNotAllowed): assert await manager.async_reload(entry.entry_id) assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + +async def test_reauth(hass): + """Test the async_reauth_helper.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init) as mock_init: + entry.async_start_reauth( + hass, + context={"extra_context": "some_extra_context"}, + data={"extra_data": 1234}, + ) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"} + assert flows[0]["context"]["extra_context"] == "some_extra_context" + + assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234 + + # Check we can't start duplicate flows + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(flows) == 1 diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 18d5469a162..136c97808d3 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -34,7 +34,7 @@ def manager(): async def async_finish_flow(self, flow, result): """Test finish flow.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: result["source"] = flow.context.get("source") entries.append(result) return result @@ -100,7 +100,7 @@ async def test_configure_two_steps(manager): form = await manager.async_configure(form["flow_id"], ["INIT-DATA"]) form = await manager.async_configure(form["flow_id"], ["SECOND-DATA"]) - assert form["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert form["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(manager.async_progress()) == 0 assert len(manager.mock_created_entries) == 1 result = manager.mock_created_entries[0] @@ -122,7 +122,7 @@ async def test_show_form(manager): ) form = await manager.async_init("test") - assert form["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form["type"] == data_entry_flow.FlowResultType.FORM assert form["data_schema"] is schema assert form["errors"] == {"username": "Should be unique."} @@ -218,7 +218,7 @@ async def test_finish_callback_change_result_type(hass): async def async_finish_flow(self, flow, result): """Redirect to init form if count <= 1.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: if result["data"] is None or result["data"].get("count", 0) <= 1: return flow.async_show_form( step_id="init", data_schema=vol.Schema({"count": int}) @@ -230,16 +230,16 @@ async def test_finish_callback_change_result_type(hass): manager = FlowManager(hass) result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await manager.async_configure(result["flow_id"], {"count": 0}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert "result" not in result result = await manager.async_configure(result["flow_id"], {"count": 2}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"] == 2 @@ -269,7 +269,7 @@ async def test_external_step(hass, manager): ) result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert len(manager.async_progress()) == 1 assert len(manager.async_progress_by_handler("test")) == 1 assert manager.async_get(result["flow_id"])["handler"] == "test" @@ -277,7 +277,7 @@ async def test_external_step(hass, manager): # Mimic external step # Called by integrations: `hass.config_entries.flow.async_configure(…)` result = await manager.async_configure(result["flow_id"], {"title": "Hello"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP_DONE await hass.async_block_till_done() assert len(events) == 1 @@ -289,7 +289,7 @@ async def test_external_step(hass, manager): # Frontend refreshses the flow result = await manager.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Hello" @@ -326,7 +326,7 @@ async def test_show_progress(hass, manager): ) result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_one" assert len(manager.async_progress()) == 1 assert len(manager.async_progress_by_handler("test")) == 1 @@ -335,7 +335,7 @@ async def test_show_progress(hass, manager): # Mimic task one done and moving to task two # Called by integrations: `hass.config_entries.flow.async_configure(…)` result = await manager.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_two" await hass.async_block_till_done() @@ -349,7 +349,7 @@ async def test_show_progress(hass, manager): # Mimic task two done and continuing step # Called by integrations: `hass.config_entries.flow.async_configure(…)` result = await manager.async_configure(result["flow_id"], {"title": "Hello"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE await hass.async_block_till_done() assert len(events) == 2 @@ -361,7 +361,7 @@ async def test_show_progress(hass, manager): # Frontend refreshes the flow result = await manager.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Hello" @@ -374,7 +374,7 @@ async def test_abort_flow_exception(manager): raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"}) form = await manager.async_init("test") - assert form["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert form["type"] == data_entry_flow.FlowResultType.ABORT assert form["reason"] == "mock-reason" assert form["description_placeholders"] == {"placeholder": "yo"} @@ -432,7 +432,7 @@ async def test_async_has_matching_flow( context={"source": config_entries.SOURCE_HOMEKIT}, data={"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_one" assert len(manager.async_progress()) == 1 assert len(manager.async_progress_by_handler("test")) == 1 @@ -517,7 +517,7 @@ async def test_show_menu(hass, manager, menu_options): return self.async_show_form(step_id="target2") result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result["type"] == data_entry_flow.FlowResultType.MENU assert result["menu_options"] == menu_options assert result["description_placeholders"] == {"name": "Paulus"} assert len(manager.async_progress()) == 1 @@ -528,5 +528,5 @@ async def test_show_menu(hass, manager, menu_options): result = await manager.async_configure( result["flow_id"], {"next_step_id": "target1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "target1" diff --git a/tests/test_loader.py b/tests/test_loader.py index 96694c43c7f..da788e0db75 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -205,6 +205,7 @@ def test_integration_properties(hass): {"hostname": "tesla_*", "macaddress": "98ED5C*"}, {"registered_devices": True}, ], + "bluetooth": [{"manufacturer_id": 76, "manufacturer_data_start": [0x06]}], "usb": [ {"vid": "10C4", "pid": "EA60"}, {"vid": "1CF1", "pid": "0030"}, @@ -242,6 +243,9 @@ def test_integration_properties(hass): {"vid": "1A86", "pid": "7523"}, {"vid": "10C4", "pid": "8A2A"}, ] + assert integration.bluetooth == [ + {"manufacturer_id": 76, "manufacturer_data_start": [0x06]} + ] assert integration.ssdp == [ { "manufacturer": "Royal Philips Electronics", @@ -274,6 +278,7 @@ def test_integration_properties(hass): assert integration.homekit is None assert integration.zeroconf is None assert integration.dhcp is None + assert integration.bluetooth is None assert integration.usb is None assert integration.ssdp is None assert integration.mqtt is None @@ -296,6 +301,7 @@ def test_integration_properties(hass): assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}] assert integration.dhcp is None assert integration.usb is None + assert integration.bluetooth is None assert integration.ssdp is None @@ -417,6 +423,25 @@ def _get_test_integration_with_dhcp_matcher(hass, name, config_flow): ) +def _get_test_integration_with_bluetooth_matcher(hass, name, config_flow): + """Return a generated test integration with a bluetooth matcher.""" + return loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": config_flow, + "bluetooth": [ + { + "local_name": "Prodigio_*", + }, + ], + }, + ) + + def _get_test_integration_with_usb_matcher(hass, name, config_flow): """Return a generated test integration with a usb matcher.""" return loader.Integration( @@ -543,6 +568,26 @@ async def test_get_zeroconf_back_compat(hass): ] +async def test_get_bluetooth(hass): + """Verify that custom components with bluetooth are found.""" + test_1_integration = _get_test_integration_with_bluetooth_matcher( + hass, "test_1", True + ) + test_2_integration = _get_test_integration_with_dhcp_matcher(hass, "test_2", True) + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = { + "test_1": test_1_integration, + "test_2": test_2_integration, + } + bluetooth = await loader.async_get_bluetooth(hass) + bluetooth_for_domain = [ + entry for entry in bluetooth if entry["domain"] == "test_1" + ] + assert bluetooth_for_domain == [ + {"domain": "test_1", "local_name": "Prodigio_*"}, + ] + + async def test_get_dhcp(hass): """Verify that custom components with dhcp are found.""" test_1_integration = _get_test_integration_with_dhcp_matcher(hass, "test_1", True) diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 9bae6f5ebea..10861767de1 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -168,7 +168,7 @@ async def test_check_loop_async_custom(caplog): hasync.check_loop(banned_function) assert ( "Detected blocking call to banned_function inside the event loop. This is " - "causing stability issues. Please report issue to the custom component author " + "causing stability issues. Please report issue to the custom integration author " "for hue doing blocking calls at custom_components/hue/light.py, line 23: " "self.light.is_on" in caplog.text ) diff --git a/tox.ini b/tox.ini index b39caacf471..b96ab648fa2 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ isolated_build = True [testenv] basepython = {env:PYTHON3_PATH:python3} # pip version duplicated in homeassistant/package_constraints.txt -pip_version = pip>=21.0,<22.2 +pip_version = pip>=21.0,<22.3 install_command = python -m pip install --use-deprecated legacy-resolver {opts} {packages} commands = {envpython} -X dev -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs}